pax_global_header00006660000000000000000000000064141722276300014516gustar00rootroot0000000000000052 comment=62743e9125103d5cd636cccbd4dad6698cfcbb44 azure-kusto-python-3.0.1/000077500000000000000000000000001417222763000153275ustar00rootroot00000000000000azure-kusto-python-3.0.1/.github/000077500000000000000000000000001417222763000166675ustar00rootroot00000000000000azure-kusto-python-3.0.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001417222763000210525ustar00rootroot00000000000000azure-kusto-python-3.0.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000011551417222763000235460ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- #### Code Sample, a copy-pastable example if possible ```python # Your code here ``` #### Problem description [this should explain **why** the current behavior is a problem and why the expected output is a better solution.] #### If query related, does it happen on other platforms (Kusto Web UI, Kusto Explorer)? [this step is to help pin point problems that are only specific to this platform.] #### Output of ``pip freeze``
[paste the output of ``pip freeze`` here below this line]
azure-kusto-python-3.0.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011231417222763000245740ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. azure-kusto-python-3.0.1/.github/pull_request_template.md000066400000000000000000000005641417222763000236350ustar00rootroot00000000000000#### Pull Request Description _[Add a description of your pull request here]_ --- #### Future Release Comment _[Add description of your change, to include in the next release]_ _[Delete any or all irrelevant sections, e.g. if your change does not warrant a release comment at all]_ **Breaking Changes:** - None **Features:** - None **Fixes:** - Noneazure-kusto-python-3.0.1/.github/workflows/000077500000000000000000000000001417222763000207245ustar00rootroot00000000000000azure-kusto-python-3.0.1/.github/workflows/pythonpackage.yml000066400000000000000000000040421417222763000243040ustar00rootroot00000000000000# This workflow will install the dependencies, run tests and lint every push name: Python package on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: [ '3.7', '3.8', '3.9', '3.10' ] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r dev_requirements.txt pip install codecov coverage pip install ./azure-kusto-data ./azure-kusto-ingest pip freeze - uses: psf/black@stable with: options: "--check --diff --line-length 160" - name: Test with pytest run: | pip install pytest pytest -v --junit-xml pytest.xml - name: EtoE Test with pytest env: APP_ID: ${{ secrets.APP_ID }} APP_KEY: ${{ secrets.APP_KEY }} AUTH_ID: ${{ secrets.AUTH_ID }} TEST_DATABASE: ${{ secrets.TEST_DATABASE }} TEST_BLOB: ${{ secrets.TEST_BLOB }} ENGINE_CONNECTION_STRING: ${{ secrets.ENGINE_CONNECTION_STRING }} run: | pytest -v ./azure-kusto-ingest/tests/e2e.py --junit-xml pytest_e2e.xml - name: Upload Unit Test Results if: always() uses: actions/upload-artifact@v2 with: name: Unit Test Results (Python ${{ matrix.python-version }}) path: pytest*.xml publish-test-results: name: "Publish Unit Tests Results" needs: build runs-on: ubuntu-latest if: always() steps: - name: Download Artifacts uses: actions/download-artifact@v2 with: path: artifacts - name: Publish Unit Test Results uses: EnricoMi/publish-unit-test-result-action@v1 with: files: artifacts/**/*.xmlazure-kusto-python-3.0.1/.github/workflows/pythonpublish.yml000066400000000000000000000022251417222763000243600ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.7' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Publish azure-kusto-data working-directory: ./azure-kusto-data env: TWINE_USERNAME: microsoftkusto TWINE_PASSWORD: ${{ secrets.pypi_password }} run: | python setup.py sdist bdist_wheel twine upload dist/* - name: Publish azure-kusto-ingest working-directory: ./azure-kusto-ingest env: TWINE_USERNAME: microsoftkusto TWINE_PASSWORD: ${{ secrets.pypi_password }} run: | python setup.py sdist bdist_wheel twine upload dist/* azure-kusto-python-3.0.1/.gitignore000066400000000000000000000116711417222763000173250ustar00rootroot00000000000000## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ dist/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # .NET Core project.lock.json project.fragment.lock.json artifacts/ **/Properties/launchSettings.json *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Typescript v1 declaration files typings/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # Result of running python setup.py install/pip install -e RECORD.txt build/ *.egg-info/ # Wheel files *.whl *.tar.gz # pytest temp files .pytest_cache* # VS code files .vscode* #mypy files .mypy* */.tox/azure-kusto-python-3.0.1/.pylintrc000066400000000000000000000001321417222763000171700ustar00rootroot00000000000000# https://github.com/getsentry/responses/issues/74 [TYPECHECK] ignored-classes= responses azure-kusto-python-3.0.1/CODE_OF_CONDUCT.md000066400000000000000000000006741417222763000201350ustar00rootroot00000000000000# Microsoft Open Source Code of Conduct This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Resources: - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns azure-kusto-python-3.0.1/CONTRIBUTING.md000066400000000000000000000042661417222763000175700ustar00rootroot00000000000000# Contributing to Azure Python SDK If you would like to become an active contributor to this project please follow the instructions provided in [Microsoft Azure Projects Contribution Guidelines](https://azure.github.io/azure-sdk/python_documentation.html). ## Requirements In order to work on this project, we recommend using the dev requirements: ```bash pip install -r dev_requirements.txt ``` These including testing related packages as well as styling ([black](https://black.readthedocs.io/en/stable/)) ## Building and Testing This project uses [pytest](https://docs.pytest.org/en/latest/). Since the tests use the package as a third-party, the easiest way to set it up is installing it in edit mode: ```bash pip install -e ./azure-kusto-data ./azure-kusto-ingest ``` After which, running tests is simple. Just run: ```bash pytest ./azure-kusto-data ./azure-kusto-ingest ``` ## Style We use black, and allow for line-length of 160, so please run: ```bash black --line-length=160 ./azure-kusto-data ./azure-kusto-ingest ``` ## PRs We welcome contributions. In order to make the PR process efficient, please follow the below checklist: * **There is an issue open concerning the code added** - (either bug or enhancement). Preferably there is an agreed upon approach in the issue. * **PR comment explains the changes done** - (This should be a TL;DR; as the rest of it should be documented in the related issue). * **PR is concise** - try and avoid make drastic changes in a single PR. Split it into multiple changes if possible. If you feel a major change is needed, it is ok, but make sure commit history is clear and one of the maintainers can comfortably review both the code and the logic behind the change. * **Please provide any related information needed to understand the change** - docs, guidelines, use-case, best practices and so on. Opinions are accepted, but have to be backed up. * **Checks should pass** - these including linting with black and running tests. ## Code of Conduct This project's code of conduct can be found in the [CODE_OF_CONDUCT.md file](https://github.com/Azure/azure-sdk-for-python/blob/master/CODE_OF_CONDUCT.md) (v1.4.0 of the http://contributor-covenant.org/ CoC). azure-kusto-python-3.0.1/LICENSE000066400000000000000000000022121417222763000163310ustar00rootroot00000000000000 MIT License Copyright (c) Microsoft Corporation. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE azure-kusto-python-3.0.1/README.md000066400000000000000000000124051417222763000166100ustar00rootroot00000000000000# Microsoft Azure Kusto (Azure Data Explorer) SDK for Python [*azure-kusto-data*]("https://github.com/Azure/azure-kusto-python/tree/master/azure-kusto-data") Package provides the capability to query Kusto clusters with Python.
[![PyPI version](https://badge.fury.io/py/azure-kusto-data.svg)](https://badge.fury.io/py/azure-kusto-data) [![Downloads](https://pepy.tech/badge/azure-kusto-data)](https://pepy.tech/project/azure-kusto-data)
[*azure-kusto-ingest*]("https://github.com/Azure/azure-kusto-python/tree/master/azure-kusto-ingest") Package allows sending data to Kusto service - i.e. ingest data.
[![PyPI version](https://badge.fury.io/py/azure-kusto-ingest.svg)](https://badge.fury.io/py/azure-kusto-ingest) [![Downloads](https://pepy.tech/badge/azure-kusto-ingest)](https://pepy.tech/project/azure-kusto-ingest)
## Install ### Option 1: Via PyPi To install via the Python Package Index (PyPI), type: * `pip install azure-kusto-data` * `pip install azure-kusto-ingest` ### Option 2: Source Via Git To get the source code of the SDK via git just type: ```python git clone git://github.com/Azure/azure-kusto-python.git cd ./azure-kusto-python/azure-kusto-data python setup.py install cd ../azure-kusto-ingest python setup.py install ``` ### Option 3: Source Zip Download a zip of the code via GitHub or PyPi. Then follow the same instructions in option 2. ### Optionals: * [_Pandas_](http://pandas.pydata.org/) - Package provides extra functionality for use with pandas. Since these are optional dependencies, install with pandas: * `pip install azure-kusto-data[pandas]` * `pip install azure-kusto-ingest[pandas]` ## Minimum Requirements * Python 3.5 and above * See setup.py for dependencies ## Authentication methods: * AAD Username/password - Provide your AAD username and password to Kusto client (**check the notice below**). * AAD application - Provide app ID and app secret to Kusto client. * AAD code - Provide only your AAD username, and authenticate yourself using a code, generated by ADAL. * AZ CLI - For those already using [azure-cli](https://github.com/Azure/azure-cli), provide access token for the logged in user`. ** IMPORTANT NOTICE **: User authentication (using username and password) has a major caveat: Sometimes users are required to use Multi-Factor Authentication. In such a case, this flow won't work for them. It is a limitation of the AAD library we are using under the hood. There are [several bugs reported](https://github.com/AzureAD/azure-activedirectory-library-for-python/issues?utf8=%E2%9C%93&q=is%3Aissue+mfa). There is also a feature request for the adal team to work on implementing IWA (Intergrated Windows Auth) so that signed in users won't have to authenticate. Feel free to [upvote](https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/31) if it is relevant in your case. ## Samples: * [Kusto Quick Start Sample App](https://github.com/Azure/azure-kusto-python/tree/master/quick_start) * [Kusto query sample sinppets](https://github.com/Azure/azure-kusto-python/blob/master/azure-kusto-data/tests/sample.py) * [Data ingest sample sinppets](https://github.com/Azure/azure-kusto-python/blob/master/azure-kusto-ingest/tests/sample.py) ## Best Practices See the SDK [best practices guide](https://docs.microsoft.com/azure/data-explorer/kusto/api/netfx/kusto-ingest-best-practices), which though written for the .NET SDK, applies similarly here. ## Need Support? - **Have a feature request for SDKs?** Please post it on [User Voice](https://feedback.azure.com/forums/915733-azure-data-explorer) to help us prioritize - **Have a technical question?** Ask on [Stack Overflow with tag "azure-data-explorer"](https://stackoverflow.com/questions/tagged/azure-data-explorer) - **Need Support?** Every customer with an active Azure subscription has access to [support](https://docs.microsoft.com/en-us/azure/azure-supportability/how-to-create-azure-support-request) with guaranteed response time. Consider submitting a ticket and get assistance from Microsoft support team - **Found a bug?** Please help us fix it by thoroughly documenting it and [filing an issue](https://github.com/Azure/azure-kusto-python/issues/new). ## Looking for SDKs for other languages/platforms? - [Node](https://github.com/azure/azure-kusto-node) - [Java](https://github.com/azure/azure-kusto-java) - [.NET](https://docs.microsoft.com/en-us/azure/kusto/api/netfx/about-the-sdk) - [Go](https://github.com/Azure/azure-kusto-go) # Contribute We gladly accept community contributions. - Issues: Please report bugs using the Issues section of GitHub - Forums: Interact with the development teams on StackOverflow or the Microsoft Azure Forums - Source Code Contributions: If you would like to become an active contributor to this project please follow the instructions provided in [Contributing.md](CONTRIBUTING.md). This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. For general suggestions about Microsoft Azure please use our [UserVoice forum](http://feedback.azure.com/forums/34192--general-feedback). azure-kusto-python-3.0.1/azure-kusto-data/000077500000000000000000000000001417222763000205275ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/MANIFEST.in000066400000000000000000000000471417222763000222660ustar00rootroot00000000000000include *.rst include azure/__init__.pyazure-kusto-python-3.0.1/azure-kusto-data/README.rst000066400000000000000000000053601417222763000222220ustar00rootroot00000000000000Microsoft Azure Kusto Library for Python ======================================== Overview -------- .. code-block:: python from azure.kusto.data import KustoClient, KustoConnectionStringBuilder cluster = "" client_id = "" client_secret = "" authority_id = "" kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(cluster, client_id, client_secret, authority_id) client = KustoClient(kcsb) db = "Samples" query = "StormEvents | take 10" response = client.execute(db, query) for row in response.primary_results[0]: print(row[0], " ", row["EventType"]) *Kusto Python Client* Library provides the capability to query Kusto clusters using Python. It is Python 3.x compatible and supports all data types through familiar Python DB API interface. It's possible to use the library, for instance, from `Jupyter Notebooks `_. which are attached to Spark clusters, including, but not exclusively, `Azure Databricks `_. instances. Async Client ~~~~~~~~~~~~ Kusto now provides an asynchronous client for queries. To use the client, first install the package with the "aio" extra: .. code:: bash pip install azure-kusto-data[aio] The async client uses exact same interface as the regular client, except that it lives in the ``azure.kusto.data.aio`` namespace, and it returns ``Futures`` you will need to ``await`` its .. code:: python from azure.kusto.data import KustoConnectionStringBuilder from azure.kusto.data.aio import KustoClient cluster = "" client_id = "" client_secret = "" authority_id = "" async def sample(): kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(cluster, client_id, client_secret, authority_id) async with KustoClient(kcsb) as client: db = "Samples" query = "StormEvents | take 10" response = await client.execute(db, query) for row in response.primary_results[0]: print(row[0], " ", row["EventType"]) Links ~~~~~ * `How to install the package `_. * `Kusto query sample `_. * `GitHub Repository `_. azure-kusto-python-3.0.1/azure-kusto-data/azure/000077500000000000000000000000001417222763000216555ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/azure/__init__.py000066400000000000000000000003611417222763000237660ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License """Init file for azure namespace. https://github.com/Azure/azure-sdk-for-python/wiki/Azure-packaging""" __path__ = __import__("pkgutil").extend_path(__path__, __name__) azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/000077500000000000000000000000001417222763000230225ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/__init__.py000066400000000000000000000003611417222763000251330ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License """Init file for azure namespace. https://github.com/Azure/azure-sdk-for-python/wiki/Azure-packaging""" __path__ = __import__("pkgutil").extend_path(__path__, __name__) azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/000077500000000000000000000000001417222763000237335ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/__init__.py000066400000000000000000000003621417222763000260450ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. from ._version import VERSION as __version__ from .client import KustoClient, KustoConnectionStringBuilder, ClientRequestProperties from .data_format import DataFormat azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/_cloud_settings.py000066400000000000000000000111111417222763000274650ustar00rootroot00000000000000import os from threading import Lock from typing import Optional, Dict from urllib.parse import urljoin import requests from azure.kusto.data.exceptions import KustoServiceError METADATA_ENDPOINT = "v1/rest/auth/metadata" DEFAULT_AUTH_ENV_VAR_NAME = "AadAuthorityUri" DEFAULT_KUSTO_CLIENT_APP_ID = "db662dc1-0cfe-4e1c-a843-19a68e65be58" DEFAULT_PUBLIC_LOGIN_URL = "https://login.microsoftonline.com" DEFAULT_REDIRECT_URI = "https://microsoft/kustoclient" DEFAULT_KUSTO_SERVICE_RESOURCE_ID = "https://kusto.kusto.windows.net" DEFAULT_FIRST_PARTY_AUTHORITY_URL = "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a" class CloudInfo: """This class holds the data for a specific cloud instance.""" def __init__( self, login_endpoint: str, login_mfa_required: bool, kusto_client_app_id: str, kusto_client_redirect_uri: str, kusto_service_resource_id: str, first_party_authority_url: str, ): self.login_endpoint = login_endpoint self.login_mfa_required = login_mfa_required self.kusto_client_app_id = kusto_client_app_id self.kusto_client_redirect_uri = kusto_client_redirect_uri # will be used for interactive login self.kusto_service_resource_id = kusto_service_resource_id self.first_party_authority_url = first_party_authority_url def authority_uri(self, authority_id: Optional[str]): return self.login_endpoint + "/" + (authority_id or "organizations") def __eq__(self, other): if not isinstance(other, self.__class__): return False return ( self.login_endpoint == other.login_endpoint and self.login_mfa_required == other.login_mfa_required and self.kusto_client_app_id == other.kusto_client_app_id and self.kusto_client_redirect_uri == other.kusto_client_redirect_uri and self.kusto_service_resource_id == other.kusto_service_resource_id and self.first_party_authority_url == other.first_party_authority_url ) class CloudSettings: """This class holds data for all cloud instances, and returns the specific data instance by parsing the dns suffix from a URL""" _cloud_info = None _cloud_cache = {} _cloud_cache_lock = Lock() DEFAULT_CLOUD = CloudInfo( login_endpoint=os.environ.get(DEFAULT_AUTH_ENV_VAR_NAME, DEFAULT_PUBLIC_LOGIN_URL), login_mfa_required=False, kusto_client_app_id=DEFAULT_KUSTO_CLIENT_APP_ID, kusto_client_redirect_uri=DEFAULT_REDIRECT_URI, kusto_service_resource_id=DEFAULT_KUSTO_SERVICE_RESOURCE_ID, first_party_authority_url=DEFAULT_FIRST_PARTY_AUTHORITY_URL, ) @classmethod def get_cloud_info_for_cluster(cls, kusto_uri: str, proxies: Optional[Dict[str, str]] = None) -> CloudInfo: if kusto_uri in cls._cloud_cache: # Double-checked locking to avoid unnecessary lock access return cls._cloud_cache[kusto_uri] with cls._cloud_cache_lock: if kusto_uri in cls._cloud_cache: return cls._cloud_cache[kusto_uri] result = requests.get(urljoin(kusto_uri, METADATA_ENDPOINT), proxies=proxies) if result.status_code == 200: content = result.json() if content is None or content == {}: raise KustoServiceError("Kusto returned an invalid cloud metadata response", result) root = content["AzureAD"] if root is not None: cls._cloud_cache[kusto_uri] = CloudInfo( login_endpoint=root["LoginEndpoint"], login_mfa_required=root["LoginMfaRequired"], kusto_client_app_id=root["KustoClientAppId"], kusto_client_redirect_uri=root["KustoClientRedirectUri"], kusto_service_resource_id=root["KustoServiceResourceId"], first_party_authority_url=root["FirstPartyAuthorityUrl"], ) else: cls._cloud_cache[kusto_uri] = cls.DEFAULT_CLOUD elif result.status_code == 404: # For now as long not all proxies implement the metadata endpoint, if no endpoint exists return public cloud data cls._cloud_cache[kusto_uri] = cls.DEFAULT_CLOUD else: raise KustoServiceError("Kusto returned an invalid cloud metadata response", result) return cls._cloud_cache[kusto_uri] azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/_converters.py000066400000000000000000000017771417222763000266520ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. import re from datetime import timedelta from dateutil import parser # Regex for TimeSpan _TIMESPAN_PATTERN = re.compile(r"(-?)((?P[0-9]*).)?(?P[0-9]{2}):(?P[0-9]{2}):(?P[0-9]{2}(\.[0-9]+)?$)") def to_datetime(value): """Converts a string to a datetime.""" if isinstance(value, int): return parser.parse(value) return parser.isoparse(value) def to_timedelta(value): """Converts a string to a timedelta.""" if isinstance(value, (int, float)): return timedelta(microseconds=(float(value) / 10)) match = _TIMESPAN_PATTERN.match(value) if match: if match.group(1) == "-": factor = -1 else: factor = 1 return factor * timedelta(days=int(match.group("d") or 0), hours=int(match.group("h")), minutes=int(match.group("m")), seconds=float(match.group("s"))) else: raise ValueError("Timespan value '{}' cannot be decoded".format(value)) azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/_decorators.py000066400000000000000000000005161417222763000266130ustar00rootroot00000000000000def aio_documented_by(original): def wrapper(target): target.__doc__ = "Aio function: {original_doc}".format(original_doc=original.__doc__) return target return wrapper def documented_by(original): def wrapper(target): target.__doc__ = original.__doc__ return target return wrapper azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/_models.py000066400000000000000000000162341417222763000257350ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. import json from abc import ABCMeta, abstractmethod from decimal import Decimal from enum import Enum from typing import Iterator, List, Any, Union, Optional, Dict from . import _converters from .exceptions import KustoMultiApiError, KustoStreamingQueryError class WellKnownDataSet(Enum): """Categorizes data tables according to the role they play in the data set that a Kusto query returns.""" PrimaryResult = "PrimaryResult" QueryCompletionInformation = "QueryCompletionInformation" TableOfContents = "TableOfContents" QueryProperties = "QueryProperties" class KustoResultRow: """Iterator over a Kusto result row.""" conversion_funcs = {"datetime": _converters.to_datetime, "timespan": _converters.to_timedelta, "decimal": Decimal} def __init__(self, columns: "List[KustoResultColumn]", row: list): self._value_by_name = {} self._value_by_index = [] for i, value in enumerate(row): column = columns[i] try: column_type = column.column_type.lower() except AttributeError: self._value_by_index.append(value) self._value_by_name[columns[i]] = value continue # If you are here to read this, you probably hit some datetime/timedelta inconsistencies. # Azure-Data-Explorer(Kusto) supports 7 decimal digits, while the corresponding python types supports only 6. # One example why one might want this precision, is when working with pandas. # In that case, use azure.kusto.data.helpers.dataframe_from_result_table which takes into account the original value. typed_value = self.get_typed_value(column_type, value) self._value_by_index.append(typed_value) self._value_by_name[column.column_name] = typed_value @staticmethod def get_typed_value(column_type: str, value: Any) -> Any: return KustoResultRow.conversion_funcs[column_type](value) if value is not None and column_type in KustoResultRow.conversion_funcs else value @property def columns_count(self) -> int: return len(self._value_by_name) def __iter__(self) -> Iterator[Any]: for i in range(self.columns_count): yield self[i] def __getitem__(self, key: Union[str, int]) -> Any: if isinstance(key, int): return self._value_by_index[key] return self._value_by_name[key] def __len__(self) -> int: return self.columns_count def to_dict(self) -> Dict[str, Any]: return self._value_by_name def to_list(self) -> list: return self._value_by_index def __str__(self) -> str: return "['{}']".format("', '".join([str(val) for val in self._value_by_index])) def __repr__(self) -> str: values = [repr(val) for val in self._value_by_name.values()] return "KustoResultRow(['{}'], [{}])".format("', '".join(self._value_by_name), ", ".join(values)) def __eq__(self, other) -> bool: if len(self) != len(other): return False for value_index, value in enumerate(self): if value != other[value_index]: return False return True class KustoResultColumn: def __init__(self, json_column: Dict[str, Any], ordinal: int): self.column_name = json_column["ColumnName"] self.column_type = json_column.get("ColumnType") or json_column["DataType"] self.ordinal = ordinal def __repr__(self) -> str: return "KustoResultColumn({},{})".format(json.dumps({"ColumnName": self.column_name, "ColumnType": self.column_type}), self.ordinal) class BaseKustoResultTable(metaclass=ABCMeta): def __init__(self, json_table: Dict[str, Any]): self.table_name = json_table.get("TableName") self.table_id = json_table.get("TableId") self.table_kind = WellKnownDataSet[json_table["TableKind"]] if "TableKind" in json_table else None self.columns = [KustoResultColumn(column, index) for index, column in enumerate(json_table["Columns"])] self.raw_columns = json_table["Columns"] self.raw_rows = json_table["Rows"] self.kusto_result_rows = None def __bool__(self) -> bool: return any(self.columns) __nonzero__ = __bool__ @property def columns_count(self) -> int: return len(self.columns) @abstractmethod def __len__(self) -> Optional[int]: pass @property @abstractmethod def rows_count(self) -> int: pass class BaseStreamingKustoResultTable(BaseKustoResultTable): def __init__(self, json_table: Dict[str, Any]): super().__init__(json_table) self.finished = False self.row_count = 0 @property def rows_count(self) -> int: if not self.finished: raise KustoStreamingQueryError("Can't retrieve rows count before the iteration is finished") return self.row_count def __len__(self) -> Optional[int]: if not self.finished: return None # We return None here instead of an exception, because otherwise calling list() on the object will throw return self.rows_count def iter_rows(self) -> "BaseStreamingKustoResultTable": return self class KustoResultTable(BaseKustoResultTable): """Iterator over a Kusto result table.""" def __init__(self, json_table: Dict[str, Any]): super().__init__(json_table) errors = [row for row in json_table["Rows"] if isinstance(row, dict)] if errors: raise KustoMultiApiError(errors) @property def rows(self) -> List[KustoResultRow]: if not self.kusto_result_rows: self.kusto_result_rows = [KustoResultRow(self.columns, row) for row in self.raw_rows] return self.kusto_result_rows def to_dict(self) -> Dict[str, Any]: """Converts the table to a dict.""" return {"name": self.table_name, "kind": self.table_kind, "data": [r.to_dict() for r in self]} @property def rows_count(self) -> int: return len(self.raw_rows) def __len__(self) -> int: return self.rows_count def __iter__(self) -> Iterator[KustoResultRow]: for row_index, row in enumerate(self.raw_rows): if self.kusto_result_rows: yield self.kusto_result_rows[row_index] else: yield KustoResultRow(self.columns, row) def __getitem__(self, key: int) -> KustoResultRow: return self.rows[key] def __str__(self) -> str: d = self.to_dict() # enum is not serializable, using value instead d["kind"] = d["kind"].value return json.dumps(d, default=str) class KustoStreamingResultTable(BaseStreamingKustoResultTable): """ Iterator over a Kusto result table in streaming. This class can be iterated only once. """ def __next__(self) -> KustoResultRow: try: row = next(self.raw_rows) except StopIteration: self.finished = True raise self.row_count += 1 return KustoResultRow(self.columns, row) def __iter__(self) -> Iterator[KustoResultRow]: return self azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/_token_providers.py000066400000000000000000000551561417222763000276750ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import abc import asyncio import time import webbrowser from threading import Lock from typing import Callable, Optional, Coroutine, List from azure.core.exceptions import ClientAuthenticationError from azure.identity import ManagedIdentityCredential, AzureCliCredential from msal import ConfidentialClientApplication, PublicClientApplication from ._cloud_settings import CloudSettings, CloudInfo from .exceptions import KustoClientError, KustoAioSyntaxError, KustoAsyncUsageError try: from asgiref.sync import sync_to_async except ImportError: def sync_to_async(f): raise KustoAioSyntaxError() try: from azure.identity.aio import ManagedIdentityCredential as AsyncManagedIdentityCredential, AzureCliCredential as AsyncAzureCliCredential except ImportError: # These are here in case the user doesn't have the aio optional dependency installed, but still tries to use async. # They will give them a useful error message, and will appease linters. class AsyncManagedIdentityCredential: def __init__(self): raise KustoAioSyntaxError() class AsyncAzureCliCredential: def __init__(self): raise KustoAioSyntaxError() # constant key names and values used throughout the code class TokenConstants: BEARER_TYPE = "Bearer" MSAL_TOKEN_TYPE = "token_type" MSAL_ACCESS_TOKEN = "access_token" MSAL_ERROR = "error" MSAL_ERROR_DESCRIPTION = "error_description" MSAL_PRIVATE_CERT = "private_key" MSAL_THUMBPRINT = "thumbprint" MSAL_PUBLIC_CERT = "public_certificate" MSAL_DEVICE_MSG = "message" MSAL_DEVICE_URI = "verification_uri" MSAL_INTERACTIVE_PROMPT = "select_account" AZ_TOKEN_TYPE = "tokenType" AZ_ACCESS_TOKEN = "accessToken" class TokenProviderBase(abc.ABC): """ This base class abstracts token acquisition for all implementations. The class is build for Lazy initialization, so that the first call, take on instantiation of 'heavy' long-lived class members """ _initialized: bool = False _resources_initialized: bool = False def __init__(self, is_async: bool = False): self._proxy_dict: Optional[str, str] = None self.is_async = is_async if is_async: self._async_lock = asyncio.Lock() else: self._lock = Lock() def _init_once(self, init_only_resources=False): if self._initialized: return with self._lock: if self._initialized: return if not self._resources_initialized: self._init_resources() self._resources_initialized = True if init_only_resources: return self._init_impl() self._initialized = True async def _init_once_async(self, init_only_resources=False): if self._initialized: return async with self._async_lock: if self._initialized: return if not self._resources_initialized: await (sync_to_async(self._init_resources)()) self._resources_initialized = True if init_only_resources: return self._init_impl() self._initialized = True def _init_resources(self): pass def get_token(self): """Get a token silently from cache or authenticate if cached token is not found""" if self.is_async: raise KustoAsyncUsageError("get_token", self.is_async) self._init_once() token = self._get_token_from_cache_impl() if token is None: with self._lock: token = self._get_token_impl() return self._valid_token_or_throw(token) def context(self) -> dict: if self.is_async: raise KustoAsyncUsageError("context", self.is_async) self._init_once(init_only_resources=True) return self._context_impl() async def context_async(self) -> dict: if not self.is_async: raise KustoAsyncUsageError("context_async", self.is_async) await self._init_once_async(init_only_resources=True) return self._context_impl() async def get_token_async(self): """Get a token asynchronously silently from cache or authenticate if cached token is not found""" if not self.is_async: raise KustoAsyncUsageError("get_token_async", self.is_async) await self._init_once_async() token = self._get_token_from_cache_impl() if token is None: async with self._async_lock: token = await self._get_token_impl_async() return self._valid_token_or_throw(token) @staticmethod @abc.abstractmethod def name() -> str: """return the provider class name""" pass @abc.abstractmethod def _context_impl(self) -> dict: """return a secret-free context for error reporting""" pass @abc.abstractmethod def _init_impl(self): """Implement any "heavy" first time initializations here""" pass @abc.abstractmethod def _get_token_impl(self) -> Optional[dict]: """implement actual token acquisition here""" pass async def _get_token_impl_async(self) -> Optional[dict]: """implement actual token acquisition here""" return await sync_to_async(self._get_token_impl)() @abc.abstractmethod def _get_token_from_cache_impl(self) -> Optional[dict]: """Implement cache checks here, return None if cache check fails""" pass @staticmethod def _valid_token_or_none(token: dict) -> Optional[dict]: if token is None or TokenConstants.MSAL_ERROR in token: return None return token def _valid_token_or_throw(self, token: dict, context: str = "") -> dict: if token is None: raise KustoClientError(self.name() + " - failed to obtain a token. " + context) if TokenConstants.MSAL_ERROR in token: message = self.name() + " - failed to obtain a token. " + context + "\n" + token[TokenConstants.MSAL_ERROR] if TokenConstants.MSAL_ERROR_DESCRIPTION in token: message = message + "\n" + token[TokenConstants.MSAL_ERROR_DESCRIPTION] raise KustoClientError(message) return token def set_proxy(self, proxy_url: str): self._proxy_dict = {"http": proxy_url, "https": proxy_url} class CloudInfoTokenProvider(TokenProviderBase, abc.ABC): _cloud_info: Optional[CloudInfo] _scopes = List[str] _kusto_uri: str def __init__(self, kusto_uri: str, is_async: bool = False): super().__init__(is_async) self._kusto_uri = kusto_uri def _init_resources(self): if self._kusto_uri is not None: self._cloud_info = CloudSettings.get_cloud_info_for_cluster(self._kusto_uri, self._proxy_dict) resource_uri = self._cloud_info.kusto_service_resource_id if self._cloud_info.login_mfa_required: resource_uri = resource_uri.replace(".kusto.", ".kustomfa.") self._scopes = [resource_uri + "/.default"] class BasicTokenProvider(TokenProviderBase): """Basic Token Provider keeps and returns a token received on construction""" def __init__(self, token: str, is_async: bool = False): super().__init__(is_async) self._token = token @staticmethod def name() -> str: return "BasicTokenProvider" def _context_impl(self) -> dict: return {"authority": self.name()} def _init_impl(self): pass def _get_token_impl(self) -> Optional[dict]: return None def _get_token_from_cache_impl(self) -> dict: return {TokenConstants.MSAL_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.MSAL_ACCESS_TOKEN: self._token} class CallbackTokenProvider(TokenProviderBase): """Callback Token Provider generates a token based on a callback function provided by the caller""" def __init__( self, token_callback: Optional[Callable[[], str]], async_token_callback: Optional[Callable[[], Coroutine[None, None, str]]], is_async: bool = False ): super().__init__(is_async) self._token_callback = token_callback self._async_token_callback = async_token_callback @staticmethod def name() -> str: return "CallbackTokenProvider" def _context_impl(self) -> dict: return {"authority": self.name()} def _init_impl(self): pass @staticmethod def _build_response(caller_token) -> dict: if not isinstance(caller_token, str): raise KustoClientError("Token provider returned something that is not a string [" + str(type(caller_token)) + "]") return {TokenConstants.MSAL_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.MSAL_ACCESS_TOKEN: caller_token} def _get_token_impl(self) -> Optional[dict]: if self._token_callback is None: raise KustoClientError("token_callback is None, can't retrieve token") return self._build_response(self._token_callback()) async def _get_token_impl_async(self) -> Optional[dict]: if self._async_token_callback is None: return await super()._get_token_impl_async() return self._build_response(await self._async_token_callback()) def _get_token_from_cache_impl(self) -> Optional[dict]: return None class MsiTokenProvider(CloudInfoTokenProvider): """ MSI Token Provider obtains a token from the MSI endpoint The args parameter is a dictionary conforming with the ManagedIdentityCredential initializer API arguments """ def __init__(self, kusto_uri: str, msi_args: dict = None, is_async: bool = False): super().__init__(kusto_uri, is_async) self._msi_args = msi_args self._msi_auth_context = None self._msi_auth_context_async = None @staticmethod def name() -> str: return "MsiTokenProvider" def _context_impl(self) -> dict: context = self._msi_args.copy() context["authority"] = self.name() return context def _init_impl(self): pass def _get_token_impl(self) -> Optional[dict]: try: if self._msi_auth_context is None: self._msi_auth_context = ManagedIdentityCredential(**self._msi_args) msi_token = self._msi_auth_context.get_token(self._scopes[0]) return {TokenConstants.MSAL_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.MSAL_ACCESS_TOKEN: msi_token.token} except ClientAuthenticationError as e: raise KustoClientError("Failed to initialize MSI ManagedIdentityCredential with [{0}]\n{1}".format(self._msi_args, e)) except Exception as e: raise KustoClientError("Failed to obtain MSI token for '{0}' with [{1}]\n{2}".format(self._kusto_uri, self._msi_args, e)) async def _get_token_impl_async(self) -> Optional[dict]: try: if self._msi_auth_context_async is None: self._msi_auth_context_async = AsyncManagedIdentityCredential(**self._msi_args) msi_token = await self._msi_auth_context_async.get_token(self._scopes[0]) return {TokenConstants.MSAL_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.MSAL_ACCESS_TOKEN: msi_token.token} except ClientAuthenticationError as e: raise KustoClientError("Failed to initialize MSI async ManagedIdentityCredential with [{0}]\n{1}".format(self._msi_args, e)) except Exception as e: raise KustoClientError("Failed to obtain MSI token for '{0}' with [{1}]\n{2}".format(self._kusto_uri, self._msi_args, e)) def _get_token_from_cache_impl(self) -> Optional[dict]: return None class AzCliTokenProvider(CloudInfoTokenProvider): """AzCli Token Provider obtains a refresh token from the AzCli cache and uses it to authenticate with MSAL""" def __init__(self, kusto_uri: str, is_async: bool = False): super().__init__(kusto_uri, is_async) self._az_auth_context = None self._az_auth_context_async = None self._az_token = None @staticmethod def name() -> str: return "AzCliTokenProvider" def _context_impl(self) -> dict: return {"authority:": self.name()} def _init_impl(self): pass def _get_token_impl(self) -> Optional[dict]: try: if self._az_auth_context is None: self._az_auth_context = AzureCliCredential() self._az_token = self._az_auth_context.get_token(self._scopes[0]) return {TokenConstants.AZ_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.AZ_ACCESS_TOKEN: self._az_token.token} except Exception as e: raise KustoClientError( "Failed to obtain Az Cli token for '{0}'.\nPlease be sure AzCli version 2.3.0 and above is intalled.\n{1}".format(self._kusto_uri, e) ) async def _get_token_impl_async(self) -> Optional[dict]: try: if self._az_auth_context_async is None: self._az_auth_context_async = AsyncAzureCliCredential() self._az_token = await self._az_auth_context_async.get_token(self._scopes[0]) return {TokenConstants.AZ_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.AZ_ACCESS_TOKEN: self._az_token.token} except Exception as e: raise KustoClientError( "Failed to obtain Az Cli token for '{0}'.\nPlease be sure AzCli version 2.3.0 and above is installed.\n{1}".format(self._kusto_uri, e) ) def _get_token_from_cache_impl(self) -> Optional[dict]: if self._az_token is not None: # A token is considered valid if it is due to expire in no less than 10 minutes cur_time = time.time() if (self._az_token.expires_on - 600) > cur_time: return {TokenConstants.MSAL_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.MSAL_ACCESS_TOKEN: self._az_token.token} return None class UserPassTokenProvider(CloudInfoTokenProvider): """Acquire a token from MSAL with username and password""" def __init__(self, kusto_uri: str, authority_id: str, username: str, password: str, is_async: bool = False): super().__init__(kusto_uri, is_async) self._msal_client = None self._auth = authority_id self._user = username self._pass = password @staticmethod def name() -> str: return "UserPassTokenProvider" def _context_impl(self) -> dict: return {"authority": self._cloud_info.authority_uri(self._auth), "client_id": self._cloud_info.kusto_client_app_id, "username": self._user} def _init_impl(self): self._msal_client = PublicClientApplication( client_id=self._cloud_info.kusto_client_app_id, authority=self._cloud_info.authority_uri(self._auth), proxies=self._proxy_dict ) def _get_token_impl(self) -> Optional[dict]: token = self._msal_client.acquire_token_by_username_password(username=self._user, password=self._pass, scopes=self._scopes) return self._valid_token_or_throw(token) def _get_token_from_cache_impl(self) -> dict: account = None if self._user is not None: accounts = self._msal_client.get_accounts(self._user) if len(accounts) > 0: account = accounts[0] token = self._msal_client.acquire_token_silent(scopes=self._scopes, account=account) return self._valid_token_or_none(token) class DeviceLoginTokenProvider(CloudInfoTokenProvider): """Acquire a token from MSAL with Device Login flow""" def __init__(self, kusto_uri: str, authority_id: str, device_code_callback=None, is_async: bool = False): super().__init__(kusto_uri, is_async) self._msal_client = None self._auth = authority_id self._account = None self._device_code_callback = device_code_callback @staticmethod def name() -> str: return "DeviceLoginTokenProvider" def _context_impl(self) -> dict: return {"authority": self._cloud_info.authority_uri(self._auth), "client_id": self._cloud_info.kusto_client_app_id} def _init_impl(self): self._msal_client = PublicClientApplication( client_id=self._cloud_info.kusto_client_app_id, authority=self._cloud_info.authority_uri(self._auth), proxies=self._proxy_dict ) def _get_token_impl(self) -> Optional[dict]: flow = self._msal_client.initiate_device_flow(scopes=self._scopes) try: if self._device_code_callback: self._device_code_callback(flow[TokenConstants.MSAL_DEVICE_MSG]) else: print(flow[TokenConstants.MSAL_DEVICE_MSG]) webbrowser.open(flow[TokenConstants.MSAL_DEVICE_URI]) except KeyError: raise KustoClientError("Failed to initiate device code flow") token = self._msal_client.acquire_token_by_device_flow(flow) # Keep the account for silent login if self._valid_token_or_none(token) is not None: accounts = self._msal_client.get_accounts() if len(accounts) == 1: self._account = accounts[0] return self._valid_token_or_throw(token) def _get_token_from_cache_impl(self) -> dict: token = self._msal_client.acquire_token_silent(scopes=self._scopes, account=self._account) return self._valid_token_or_none(token) class InteractiveLoginTokenProvider(CloudInfoTokenProvider): """Acquire a token from MSAL with Device Login flow""" def __init__( self, kusto_uri: str, authority_id: str, login_hint: Optional[str] = None, domain_hint: Optional[str] = None, is_async: bool = False, ): super().__init__(kusto_uri, is_async) self._msal_client = None self._auth = authority_id self._login_hint = login_hint self._domain_hint = domain_hint self._account = None @staticmethod def name() -> str: return "InteractiveLoginTokenProvider" def _context_impl(self) -> dict: return {"authority": self._cloud_info.authority_uri(self._auth), "client_id": self._cloud_info.kusto_client_app_id} def _init_impl(self): self._msal_client = PublicClientApplication( client_id=self._cloud_info.kusto_client_app_id, authority=self._cloud_info.authority_uri(self._auth), proxies=self._proxy_dict ) def _get_token_impl(self) -> Optional[dict]: token = self._msal_client.acquire_token_interactive( scopes=self._scopes, prompt=TokenConstants.MSAL_INTERACTIVE_PROMPT, login_hint=self._login_hint, domain_hint=self._domain_hint ) return self._valid_token_or_throw(token) def _get_token_from_cache_impl(self) -> dict: account = None accounts = self._msal_client.get_accounts(self._login_hint) if len(accounts) > 0: account = accounts[0] token = self._msal_client.acquire_token_silent(scopes=self._scopes, account=account) return self._valid_token_or_none(token) class ApplicationKeyTokenProvider(CloudInfoTokenProvider): """Acquire a token from MSAL with application Id and Key""" def __init__(self, kusto_uri: str, authority_id: str, app_client_id: str, app_key: str, is_async: bool = False): super().__init__(kusto_uri, is_async) self._msal_client = None self._app_client_id = app_client_id self._app_key = app_key self._auth = authority_id @staticmethod def name() -> str: return "ApplicationKeyTokenProvider" def _context_impl(self) -> dict: return {"authority": self._cloud_info.authority_uri(self._auth), "client_id": self._app_client_id} def _init_impl(self): self._msal_client = ConfidentialClientApplication( client_id=self._app_client_id, client_credential=self._app_key, authority=self._cloud_info.authority_uri(self._auth), proxies=self._proxy_dict ) def _get_token_impl(self) -> Optional[dict]: token = self._msal_client.acquire_token_for_client(scopes=self._scopes) return self._valid_token_or_throw(token) def _get_token_from_cache_impl(self) -> dict: token = self._msal_client.acquire_token_silent(scopes=self._scopes, account=None) return self._valid_token_or_none(token) class ApplicationCertificateTokenProvider(CloudInfoTokenProvider): """ Acquire a token from MSAL using application certificate Passing the public certificate is optional and will result in Subject Name & Issuer Authentication """ def __init__( self, kusto_uri: str, client_id: str, authority_id: str, private_cert: str, thumbprint: str, public_cert: str = None, is_async: bool = False, ): super().__init__(kusto_uri, is_async) self._msal_client = None self._auth = authority_id self._client_id = client_id self._cert_credentials = {TokenConstants.MSAL_PRIVATE_CERT: private_cert, TokenConstants.MSAL_THUMBPRINT: thumbprint} if public_cert is not None: self._cert_credentials[TokenConstants.MSAL_PUBLIC_CERT] = public_cert @staticmethod def name() -> str: return "ApplicationCertificateTokenProvider" def _context_impl(self) -> dict: return { "authority": self._cloud_info.authority_uri(self._auth), "client_id": self._client_id, "thumbprint": self._cert_credentials[TokenConstants.MSAL_THUMBPRINT], } def _init_impl(self): self._msal_client = ConfidentialClientApplication( client_id=self._client_id, client_credential=self._cert_credentials, authority=self._cloud_info.authority_uri(self._auth), proxies=self._proxy_dict ) def _get_token_impl(self) -> Optional[dict]: token = self._msal_client.acquire_token_for_client(scopes=self._scopes) return self._valid_token_or_throw(token) def _get_token_from_cache_impl(self) -> dict: token = self._msal_client.acquire_token_silent(scopes=self._scopes, account=None) return self._valid_token_or_none(token) azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/_version.py000066400000000000000000000001321417222763000261250ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License VERSION = "3.0.1" azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/aio/000077500000000000000000000000001417222763000245035ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/aio/__init__.py000066400000000000000000000000401417222763000266060ustar00rootroot00000000000000from .client import KustoClient azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/aio/_models.py000066400000000000000000000011161417222763000264760ustar00rootroot00000000000000from typing import AsyncIterator from azure.kusto.data._models import KustoResultRow, BaseStreamingKustoResultTable class KustoStreamingResultTable(BaseStreamingKustoResultTable): """Async Iterator over a Kusto result table.""" async def __anext__(self) -> KustoResultRow: try: row = await self.raw_rows.__anext__() except StopAsyncIteration: self.finished = True raise self.row_count += 1 return KustoResultRow(self.columns, row) def __aiter__(self) -> AsyncIterator[KustoResultRow]: return self azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/aio/client.py000066400000000000000000000136161417222763000263420ustar00rootroot00000000000000import io from datetime import timedelta from typing import Union, Optional from .response import KustoStreamingResponseDataSet from .._decorators import documented_by, aio_documented_by from ..aio.streaming_response import StreamingDataSetEnumerator, JsonTokenReader from ..client import KustoClient as KustoClientSync, _KustoClientBase, KustoConnectionStringBuilder, ClientRequestProperties, ExecuteRequestParams from ..data_format import DataFormat from ..exceptions import KustoAioSyntaxError from ..response import KustoResponseDataSet try: from aiohttp import ClientResponse, ClientSession except ImportError: raise KustoAioSyntaxError() @documented_by(KustoClientSync) class KustoClient(_KustoClientBase): @documented_by(KustoClientSync.__init__) def __init__(self, kcsb: Union[KustoConnectionStringBuilder, str]): super().__init__(kcsb, True) self._session = ClientSession() async def __aenter__(self) -> "KustoClient": return self def __aexit__(self, exc_type, exc_val, exc_tb): return self._session.__aexit__(exc_type, exc_val, exc_tb) @aio_documented_by(KustoClientSync.execute) async def execute(self, database: str, query: str, properties: ClientRequestProperties = None) -> KustoResponseDataSet: query = query.strip() if query.startswith("."): return await self.execute_mgmt(database, query, properties) return await self.execute_query(database, query, properties) @aio_documented_by(KustoClientSync.execute_query) async def execute_query(self, database: str, query: str, properties: ClientRequestProperties = None) -> KustoResponseDataSet: return await self._execute(self._query_endpoint, database, query, None, KustoClient._query_default_timeout, properties) @aio_documented_by(KustoClientSync.execute_mgmt) async def execute_mgmt(self, database: str, query: str, properties: ClientRequestProperties = None) -> KustoResponseDataSet: return await self._execute(self._mgmt_endpoint, database, query, None, KustoClient._mgmt_default_timeout, properties) @aio_documented_by(KustoClientSync.execute_streaming_ingest) async def execute_streaming_ingest( self, database: str, table: str, stream: io.IOBase, stream_format: Union[DataFormat, str], properties: ClientRequestProperties = None, mapping_name: str = None, ): stream_format = stream_format.kusto_value if isinstance(stream_format, DataFormat) else DataFormat[stream_format.upper()].kusto_value endpoint = self._streaming_ingest_endpoint + database + "/" + table + "?streamFormat=" + stream_format if mapping_name is not None: endpoint = endpoint + "&mappingName=" + mapping_name await self._execute(endpoint, database, None, stream, self._streaming_ingest_default_timeout, properties) @aio_documented_by(KustoClientSync._execute_streaming_query_parsed) async def _execute_streaming_query_parsed( self, database: str, query: str, timeout: timedelta = _KustoClientBase._query_default_timeout, properties: Optional[ClientRequestProperties] = None ) -> StreamingDataSetEnumerator: response = await self._execute(self._query_endpoint, database, query, None, timeout, properties, stream_response=True) return StreamingDataSetEnumerator(JsonTokenReader(response.content)) @aio_documented_by(KustoClientSync.execute_streaming_query) async def execute_streaming_query( self, database: str, query: str, timeout: timedelta = _KustoClientBase._query_default_timeout, properties: Optional[ClientRequestProperties] = None ) -> KustoStreamingResponseDataSet: response = await self._execute_streaming_query_parsed(database, query, timeout, properties) return KustoStreamingResponseDataSet(response) @aio_documented_by(KustoClientSync._execute) async def _execute( self, endpoint: str, database: str, query: Optional[str], payload: Optional[io.IOBase], timeout: timedelta, properties: ClientRequestProperties = None, stream_response: bool = False, ) -> Union[KustoResponseDataSet, ClientResponse]: """Executes given query against this client""" request_params = ExecuteRequestParams(database, payload, properties, query, timeout, self._request_headers) json_payload = request_params.json_payload request_headers = request_params.request_headers timeout = request_params.timeout if self._aad_helper: request_headers["Authorization"] = await self._aad_helper.acquire_authorization_header_async() response = await self._session.post(endpoint, headers=request_headers, data=payload, json=json_payload, timeout=timeout.seconds, proxy=self._proxy_url) if stream_response: try: response.raise_for_status() return response except Exception as e: try: response_text = await response.text() except Exception: response_text = None try: response_json = await response.json() except Exception: response_json = None raise self._handle_http_error(e, endpoint, payload, response, response.status, response_json, response_text) async with response: response_json = None try: response_json = await response.json() response.raise_for_status() except Exception as e: try: response_text = await response.text() except Exception: response_text = None raise self._handle_http_error(e, endpoint, payload, response, response.status, response_json, response_text) return self._kusto_parse_by_endpoint(endpoint, response_json) azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/aio/response.py000066400000000000000000000067231417222763000267230ustar00rootroot00000000000000from typing import List, AsyncIterator, Union from azure.kusto.data._models import WellKnownDataSet, KustoResultTable, BaseKustoResultTable from azure.kusto.data.aio._models import KustoStreamingResultTable from azure.kusto.data.aio.streaming_response import StreamingDataSetEnumerator from azure.kusto.data.exceptions import KustoStreamingQueryError from azure.kusto.data.response import BaseKustoResponseDataSet from azure.kusto.data.streaming_response import FrameType class KustoStreamingResponseDataSet(BaseKustoResponseDataSet): _status_column = "Payload" _error_column = "Level" _crid_column = "ClientRequestId" def __init__(self, streamed_data: StreamingDataSetEnumerator): self._current_table = None self._skip_incomplete_tables = False self.tables = [] self.streamed_data = streamed_data self.finished = False def iter_primary_results(self) -> "PrimaryResultsIterator": return PrimaryResultsIterator(self) def __aiter__(self) -> AsyncIterator[BaseKustoResultTable]: return self async def __anext__(self) -> BaseKustoResultTable: if self.finished: raise StopAsyncIteration() if type(self._current_table) == KustoStreamingResultTable and not self._current_table.finished and not self._skip_incomplete_tables: raise KustoStreamingQueryError( "Tried retrieving a new primary_result table before the old one was finished. To override call `set_skip_incomplete_tables(True)`" ) while True: try: table = await self.streamed_data.__anext__() except StopAsyncIteration: self.finished = True return if table["FrameType"] == FrameType.DataTable: break if table["TableKind"] == WellKnownDataSet.PrimaryResult.value: self._current_table = KustoStreamingResultTable(table) else: self._current_table = KustoResultTable(table) self.tables.append(self._current_table) return self._current_table def set_skip_incomplete_tables(self, value: bool): self._skip_incomplete_tables = value @property def errors_count(self) -> int: if not self.finished: raise KustoStreamingQueryError("Unable to get errors count before reading all of the tables.") return super().errors_count def get_exceptions(self) -> List[str]: if not self.finished: raise KustoStreamingQueryError("Unable to get errors count before reading all of the tables.") return super().get_exceptions() def __getitem__(self, key: Union[int, str]) -> KustoResultTable: if isinstance(key, int): return self.tables[key] try: return next(t for t in self.tables if t.table_name == key) except StopIteration: raise LookupError(key) def __len__(self) -> int: return len(self.tables) class PrimaryResultsIterator: # This class exists because you can't raise exception from an generator and keep working def __init__(self, dataset: KustoStreamingResponseDataSet): self.dataset = dataset def __aiter__(self) -> AsyncIterator[KustoStreamingResultTable]: return self async def __anext__(self) -> KustoStreamingResultTable: while True: table = await self.dataset.__anext__() if type(table) is KustoStreamingResultTable: return table azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/aio/streaming_response.py000066400000000000000000000236121417222763000307700ustar00rootroot00000000000000from typing import Any, Tuple, Dict, Iterator import aiohttp import ijson from ijson import IncompleteJSONError from azure.kusto.data._models import WellKnownDataSet from azure.kusto.data.exceptions import KustoTokenParsingError, KustoUnsupportedApiError, KustoApiError, KustoMultiApiError from azure.kusto.data.streaming_response import JsonTokenType, FrameType, JsonToken class JsonTokenReader: def __init__(self, stream: aiohttp.StreamReader): self.json_iter = ijson.parse_async(stream, use_float=True) def __aiter__(self) -> "JsonTokenReader": return self def __anext__(self) -> JsonToken: return self.read_next_token_or_throw() async def read_next_token_or_throw(self) -> JsonToken: try: next_item = await self.json_iter.__anext__() except IncompleteJSONError: next_item = None if next_item is None: raise KustoTokenParsingError("Unexpected end of stream") (token_path, token_type, token_value) = next_item return JsonToken(token_path, JsonTokenType[token_type.upper()], token_value) async def read_token_of_type(self, *token_types: JsonTokenType) -> JsonToken: token = await self.read_next_token_or_throw() if token.token_type not in token_types: raise KustoTokenParsingError(f"Expected one the following types: '{','.join(t.name for t in token_types)}' , got type {token.token_type}") return token async def read_start_object(self) -> JsonToken: return await self.read_token_of_type(JsonTokenType.START_MAP) async def read_start_array(self) -> JsonToken: return await self.read_token_of_type(JsonTokenType.START_ARRAY) async def read_string(self) -> str: return (await self.read_token_of_type(JsonTokenType.STRING)).token_value async def read_boolean(self) -> bool: return (await self.read_token_of_type(JsonTokenType.BOOLEAN)).token_value async def read_number(self) -> float: return (await self.read_token_of_type(JsonTokenType.NUMBER)).token_value async def skip_children(self, prev_token: JsonToken): if prev_token.token_type == JsonTokenType.MAP_KEY: prev_token = await self.read_next_token_or_throw() if prev_token.token_type in JsonTokenType.start_tokens(): async for potential_end_token in self: if potential_end_token.token_path == prev_token.token_path and potential_end_token.token_type in JsonTokenType.end_tokens(): break async def skip_until_property_name(self, name: str) -> JsonToken: while True: token = await self.read_token_of_type(JsonTokenType.MAP_KEY) if token.token_value == name: return token await self.skip_children(token) async def skip_until_any_property_name(self, *names: str) -> JsonToken: while True: token = await self.read_token_of_type(JsonTokenType.MAP_KEY) if token.token_value in names: return token await self.skip_children(token) async def skip_until_property_name_or_end_object(self, *names: str) -> JsonToken: async for token in self: if token.token_type == JsonTokenType.END_MAP: return token if token.token_type == JsonTokenType.MAP_KEY: if token.token_value in names: return token await self.skip_children(token) continue raise Exception(f"Unexpected token {token}") async def skip_until_token_with_paths(self, *tokens: (JsonTokenType, str)) -> JsonToken: async for token in self: if any((token.token_type == t_type and token.token_path == t_path) for (t_type, t_path) in tokens): return token await self.skip_children(token) class StreamingDataSetEnumerator: def __init__(self, reader: JsonTokenReader): self.reader = reader self.done = False self.started = False self.started_primary_results = False self.finished_primary_results = False def __aiter__(self) -> "StreamingDataSetEnumerator": return self async def __anext__(self) -> Dict[str, Any]: if self.done: raise StopIteration() if not self.started: await self.reader.read_start_array() self.started = True token = await self.reader.skip_until_token_with_paths((JsonTokenType.START_MAP, "item"), (JsonTokenType.END_ARRAY, "")) if token == JsonTokenType.END_ARRAY: self.done = True raise StopIteration() frame_type = await self.read_frame_type() parsed_frame = await self.parse_frame(frame_type) is_primary_result = parsed_frame["FrameType"] == FrameType.DataTable and parsed_frame["TableKind"] == WellKnownDataSet.PrimaryResult.value if is_primary_result: self.started_primary_results = True elif self.started_primary_results: self.finished_primary_results = True return parsed_frame async def parse_frame(self, frame_type: FrameType) -> Dict[str, Any]: if frame_type == FrameType.DataSetHeader: frame = await self.extract_props(frame_type, ("IsProgressive", JsonTokenType.BOOLEAN), ("Version", JsonTokenType.STRING)) if frame["IsProgressive"]: raise KustoUnsupportedApiError.progressive_api_unsupported() return frame if frame_type in [FrameType.TableHeader, FrameType.TableFragment, FrameType.TableCompletion, FrameType.TableProgress]: raise KustoUnsupportedApiError.progressive_api_unsupported() if frame_type == FrameType.DataTable: props = await self.extract_props( frame_type, ("TableId", JsonTokenType.NUMBER), ("TableKind", JsonTokenType.STRING), ("TableName", JsonTokenType.STRING), ("Columns", JsonTokenType.START_ARRAY), ) await self.reader.skip_until_property_name("Rows") props["Rows"] = self.row_iterator() if props["TableKind"] != WellKnownDataSet.PrimaryResult.value: props["Rows"] = [r async for r in props["Rows"]] return props if frame_type == FrameType.DataSetCompletion: res = await self.extract_props(frame_type, ("HasErrors", JsonTokenType.BOOLEAN), ("Cancelled", JsonTokenType.BOOLEAN)) token = await self.reader.skip_until_property_name_or_end_object("OneApiErrors") if token.token_type != JsonTokenType.END_MAP: res["OneApiErrors"] = self.parse_array(skip_start=False) return res async def row_iterator(self) -> Iterator[list]: await self.reader.read_token_of_type(JsonTokenType.START_ARRAY) while True: token = await self.reader.read_token_of_type(JsonTokenType.START_ARRAY, JsonTokenType.END_ARRAY, JsonTokenType.START_MAP) if token.token_type == JsonTokenType.START_MAP: raise KustoMultiApiError([await self.parse_object(skip_start=True)]) if token.token_type == JsonTokenType.END_ARRAY: return yield await self.parse_array(skip_start=True) async def parse_array(self, skip_start: bool) -> list: if not skip_start: await self.reader.read_start_array() arr = [] while True: token = await self.reader.read_token_of_type( JsonTokenType.NULL, JsonTokenType.BOOLEAN, JsonTokenType.NUMBER, JsonTokenType.STRING, JsonTokenType.START_MAP, JsonTokenType.START_ARRAY, JsonTokenType.END_ARRAY, ) if token.token_type == JsonTokenType.END_ARRAY: return arr if token.token_type == JsonTokenType.START_MAP: arr.append(await self.parse_object(skip_start=True)) elif token.token_type == JsonTokenType.START_ARRAY: arr.append(await self.parse_array(skip_start=True)) else: arr.append(token.token_value) async def parse_object(self, skip_start: bool) -> Dict[str, Any]: if not skip_start: await self.reader.read_start_object() obj = {} while True: token_prop_name = await self.reader.read_token_of_type(JsonTokenType.MAP_KEY, JsonTokenType.END_MAP) if token_prop_name.token_type == JsonTokenType.END_MAP: return obj prop_name = token_prop_name.token_value token = await self.reader.read_token_of_type( JsonTokenType.NULL, JsonTokenType.BOOLEAN, JsonTokenType.NUMBER, JsonTokenType.STRING, JsonTokenType.START_MAP, JsonTokenType.START_ARRAY ) if token.token_type == JsonTokenType.START_MAP: obj[prop_name] = await self.parse_object(skip_start=True) elif token.token_type == JsonTokenType.START_ARRAY: obj[prop_name] = await self.parse_array(skip_start=True) else: obj[prop_name] = token.token_value async def extract_props(self, frame_type: FrameType, *props: Tuple[str, JsonTokenType]) -> Dict[str, Any]: result = {"FrameType": frame_type} props_dict = dict(props) while props_dict: name = (await self.reader.skip_until_any_property_name(*props_dict.keys())).token_value if props_dict[name] == JsonTokenType.START_ARRAY: result[name] = await self.parse_array(skip_start=False) else: result[name] = (await self.reader.read_token_of_type(props_dict[name])).token_value props_dict.pop(name) return result async def read_frame_type(self) -> FrameType: await self.reader.skip_until_property_name("FrameType") return FrameType[await self.reader.read_string()] azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/client.py000066400000000000000000001254411417222763000255720ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import abc import io import json import socket import sys import uuid from copy import copy from datetime import timedelta from enum import Enum, unique from typing import TYPE_CHECKING, Union, Callable, Optional, Any, Coroutine, List, Tuple, AnyStr, IO, NoReturn import requests from requests import Response from urllib3.connection import HTTPConnection from ._version import VERSION from .data_format import DataFormat from .exceptions import KustoServiceError, KustoApiError from .response import KustoResponseDataSetV1, KustoResponseDataSetV2, KustoStreamingResponseDataSet, KustoResponseDataSet from .security import _AadHelper from .streaming_response import StreamingDataSetEnumerator, JsonTokenReader if TYPE_CHECKING: import aiohttp import aiohttp.web_exceptions class KustoConnectionStringBuilder: """ Parses Kusto connection strings. For usages, check out the sample at: https://github.com/Azure/azure-kusto-python/blob/master/azure-kusto-data/tests/sample.py """ @unique class ValidKeywords(Enum): """ Set of properties that can be use in a connection string provided to KustoConnectionStringBuilder. For a complete list of properties go to https://docs.microsoft.com/en-us/azure/kusto/api/connection-strings/kusto """ data_source = "Data Source" aad_federated_security = "AAD Federated Security" aad_user_id = "AAD User ID" password = "Password" application_client_id = "Application Client Id" application_key = "Application Key" application_certificate = "Application Certificate" application_certificate_thumbprint = "Application Certificate Thumbprint" public_application_certificate = "Public Application Certificate" authority_id = "Authority Id" application_token = "Application Token" user_token = "User Token" msi_auth = "MSI Authentication" msi_params = "MSI Params" az_cli = "AZ CLI" interactive_login = "Interactive Login" login_hint = "Login Hint" domain_hint = "Domain Hint" @classmethod def parse(cls, key: str) -> "KustoConnectionStringBuilder.ValidKeywords": """Create a valid keyword.""" key = key.lower().strip() if key in ["data source", "addr", "address", "network address", "server"]: return cls.data_source if key in ["aad user id"]: return cls.aad_user_id if key in ["password", "pwd"]: return cls.password if key in ["application client id", "appclientid"]: return cls.application_client_id if key in ["application key", "appkey"]: return cls.application_key if key in ["application certificate"]: return cls.application_certificate if key in ["application certificate thumbprint"]: return cls.application_certificate_thumbprint if key in ["public application certificate"]: return cls.public_application_certificate if key in ["authority id", "authorityid", "authority", "tenantid", "tenant", "tid"]: return cls.authority_id if key in ["aad federated security", "federated security", "federated", "fed", "aadfed"]: return cls.aad_federated_security if key in ["application token", "apptoken"]: return cls.application_token if key in ["user token", "usertoken", "usrtoken"]: return cls.user_token if key in ["msi_auth"]: return cls.msi_auth if key in ["msi_type"]: return cls.msi_params if key in ["az cli"]: return cls.az_cli if key in ["interactive login"]: return cls.interactive_login if key in ["login hint"]: return cls.login_hint if key in ["domain hint"]: return cls.domain_hint raise KeyError(key) def is_secret(self) -> bool: """States for each property if it contains secret""" return self in [self.password, self.application_key, self.application_certificate, self.application_token, self.user_token] def is_str_type(self) -> bool: """States whether a word is of type str or not.""" return self in [ self.aad_user_id, self.application_certificate, self.application_certificate_thumbprint, self.public_application_certificate, self.application_client_id, self.data_source, self.password, self.application_key, self.authority_id, self.application_token, self.user_token, self.login_hint, self.domain_hint, ] def is_dict_type(self) -> bool: return self in [self.msi_params] def is_bool_type(self) -> bool: """States whether a word is of type bool or not.""" return self in [self.aad_federated_security, self.msi_auth, self.az_cli, self.interactive_login] def __init__(self, connection_string: str): """ Creates new KustoConnectionStringBuilder. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net;AAD User ID="user@microsoft.com";Password=P@ssWord For more information please look at: https://kusto.azurewebsites.net/docs/concepts/kusto_connection_strings.html """ _assert_value_is_valid(connection_string) self._internal_dict = {} self._token_provider = None self._async_token_provider = None if connection_string is not None and "=" not in connection_string.partition(";")[0]: connection_string = "Data Source=" + connection_string self[self.ValidKeywords.authority_id] = "common" for kvp_string in connection_string.split(";"): key, _, value = kvp_string.partition("=") keyword = self.ValidKeywords.parse(key) if keyword.is_str_type(): self[keyword] = value.strip() if keyword.is_bool_type(): if value.strip() in ["True", "true"]: self[keyword] = True elif value.strip() in ["False", "false"]: self[keyword] = False else: raise KeyError("Expected aad federated security to be bool. Recieved %s" % value) def __setitem__(self, key: "Union[KustoConnectionStringBuilder.ValidKeywords, str]", value: Union[str, bool, dict]): try: keyword = key if isinstance(key, self.ValidKeywords) else self.ValidKeywords.parse(key) except KeyError: raise KeyError("%s is not supported as an item in KustoConnectionStringBuilder" % key) if value is None: raise TypeError("Value cannot be None.") if keyword.is_str_type(): self._internal_dict[keyword] = value.strip() elif keyword.is_bool_type(): if not isinstance(value, bool): raise TypeError("Expected %s to be bool" % key) self._internal_dict[keyword] = value elif keyword.is_dict_type(): if not isinstance(value, dict): raise TypeError("Expected %s to be dict" % key) self._internal_dict[keyword] = value else: raise KeyError("KustoConnectionStringBuilder supports only bools and strings.") @classmethod def with_aad_user_password_authentication( cls, connection_string: str, user_id: str, password: str, authority_id: str = "common" ) -> "KustoConnectionStringBuilder": """ Creates a KustoConnection string builder that will authenticate with AAD user name and password. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param str user_id: AAD user ID. :param str password: Corresponding password of the AAD user. :param str authority_id: optional param. defaults to "common" """ _assert_value_is_valid(user_id) _assert_value_is_valid(password) kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb[kcsb.ValidKeywords.aad_user_id] = user_id kcsb[kcsb.ValidKeywords.password] = password kcsb[kcsb.ValidKeywords.authority_id] = authority_id return kcsb @classmethod def with_aad_user_token_authentication(cls, connection_string: str, user_token: str) -> "KustoConnectionStringBuilder": """ Creates a KustoConnection string builder that will authenticate with AAD application and a certificate credentials. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param str user_token: AAD user token. """ _assert_value_is_valid(user_token) kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb[kcsb.ValidKeywords.user_token] = user_token return kcsb @classmethod def with_aad_application_key_authentication( cls, connection_string: str, aad_app_id: str, app_key: str, authority_id: str ) -> "KustoConnectionStringBuilder": """ Creates a KustoConnection string builder that will authenticate with AAD application and key. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param str aad_app_id: AAD application ID. :param str app_key: Corresponding key of the AAD application. :param str authority_id: Authority id (aka Tenant id) must be provided """ _assert_value_is_valid(aad_app_id) _assert_value_is_valid(app_key) _assert_value_is_valid(authority_id) kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb[kcsb.ValidKeywords.application_client_id] = aad_app_id kcsb[kcsb.ValidKeywords.application_key] = app_key kcsb[kcsb.ValidKeywords.authority_id] = authority_id return kcsb @classmethod def with_aad_application_certificate_authentication( cls, connection_string: str, aad_app_id: str, certificate: str, thumbprint: str, authority_id: str ) -> "KustoConnectionStringBuilder": """ Creates a KustoConnection string builder that will authenticate with AAD application using a certificate. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param str aad_app_id: AAD application ID. :param str certificate: A PEM encoded certificate private key. :param str thumbprint: hex encoded thumbprint of the certificate. :param str authority_id: Authority id (aka Tenant id) must be provided """ _assert_value_is_valid(aad_app_id) _assert_value_is_valid(certificate) _assert_value_is_valid(thumbprint) _assert_value_is_valid(authority_id) kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb[kcsb.ValidKeywords.application_client_id] = aad_app_id kcsb[kcsb.ValidKeywords.application_certificate] = certificate kcsb[kcsb.ValidKeywords.application_certificate_thumbprint] = thumbprint kcsb[kcsb.ValidKeywords.authority_id] = authority_id return kcsb @classmethod def with_aad_application_certificate_sni_authentication( cls, connection_string: str, aad_app_id: str, private_certificate: str, public_certificate: str, thumbprint: str, authority_id: str ) -> "KustoConnectionStringBuilder": """ Creates a KustoConnection string builder that will authenticate with AAD application using a certificate Subject Name and Issuer. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param str aad_app_id: AAD application ID. :param str private_certificate: A PEM encoded certificate private key. :param str public_certificate: A public certificate matching the provided PEM certificate private key. :param str thumbprint: hex encoded thumbprint of the certificate. :param str authority_id: Authority id (aka Tenant id) must be provided """ _assert_value_is_valid(aad_app_id) _assert_value_is_valid(private_certificate) _assert_value_is_valid(public_certificate) _assert_value_is_valid(thumbprint) _assert_value_is_valid(authority_id) kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb[kcsb.ValidKeywords.application_client_id] = aad_app_id kcsb[kcsb.ValidKeywords.application_certificate] = private_certificate kcsb[kcsb.ValidKeywords.public_application_certificate] = public_certificate kcsb[kcsb.ValidKeywords.application_certificate_thumbprint] = thumbprint kcsb[kcsb.ValidKeywords.authority_id] = authority_id return kcsb @classmethod def with_aad_application_token_authentication(cls, connection_string: str, application_token: str) -> "KustoConnectionStringBuilder": """ Creates a KustoConnection string builder that will authenticate with AAD application and an application token. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param str application_token: AAD application token. """ _assert_value_is_valid(application_token) kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb[kcsb.ValidKeywords.application_token] = application_token return kcsb @classmethod def with_aad_device_authentication(cls, connection_string: str, authority_id: str = "common") -> "KustoConnectionStringBuilder": """ Creates a KustoConnection string builder that will authenticate with AAD application and password. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param str authority_id: optional param. defaults to "common" """ kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb[kcsb.ValidKeywords.authority_id] = authority_id return kcsb @classmethod def with_az_cli_authentication(cls, connection_string: str) -> "KustoConnectionStringBuilder": """ Creates a KustoConnection string builder that will use existing authenticated az cli profile password. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net """ kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.az_cli] = True kcsb[kcsb.ValidKeywords.aad_federated_security] = True return kcsb @classmethod def with_aad_managed_service_identity_authentication( cls, connection_string: str, client_id: str = None, object_id: str = None, msi_res_id: str = None, timeout: int = None ) -> "KustoConnectionStringBuilder": """ Creates a KustoConnection string builder that will authenticate with AAD application, using an application token obtained from a Microsoft Service Identity endpoint. An optional user assigned application ID can be added to the token. :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param client_id: an optional user assigned identity provided as an Azure ID of a client :param object_id: an optional user assigned identity provided as an Azure ID of an object :param msi_res_id: an optional user assigned identity provided as an Azure ID of an MSI resource :param timeout: an optional timeout (seconds) to wait for an MSI Authentication to occur """ kcsb = cls(connection_string) params = {} exclusive_pcount = 0 if timeout is not None: params["connection_timeout"] = timeout if client_id is not None: params["client_id"] = client_id exclusive_pcount += 1 if object_id is not None: # Until we upgrade azure-identity to version 1.4.1, only client_id is excepted as a hint for user managed service identity raise ValueError("User Managed Service Identity with object_id is temporarily not supported by azure identity 1.3.1. Please use client_id instead.") # noinspection PyUnreachableCode params["object_id"] = object_id exclusive_pcount += 1 if msi_res_id is not None: # Until we upgrade azure-identity to version 1.4.1, only client_id is excepted as a hint for user managed service identity raise ValueError( "User Managed Service Identity with msi_res_id is temporarily not supported by azure identity 1.3.1. Please use client_id instead." ) # noinspection PyUnreachableCode params["msi_res_id"] = msi_res_id exclusive_pcount += 1 if exclusive_pcount > 1: raise ValueError("the following parameters are mutually exclusive and can not be provided at the same time: client_uid, object_id, msi_res_id") kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb[kcsb.ValidKeywords.msi_auth] = True kcsb[kcsb.ValidKeywords.msi_params] = params return kcsb @classmethod def with_token_provider(cls, connection_string: str, token_provider: Callable[[], str]) -> "KustoConnectionStringBuilder": """ Create a KustoConnectionStringBuilder that uses a callback function to obtain a connection token :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param token_provider: a parameterless function that returns a valid bearer token for the relevant kusto resource as a string """ assert callable(token_provider) kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb._token_provider = token_provider return kcsb @classmethod def with_async_token_provider( cls, connection_string: str, async_token_provider: Callable[[], Coroutine[None, None, str]], ) -> "KustoConnectionStringBuilder": """ Create a KustoConnectionStringBuilder that uses an async callback function to obtain a connection token :param str connection_string: Kusto connection string should be of the format: https://.kusto.windows.net :param async_token_provider: a parameterless function that after awaiting returns a valid bearer token for the relevant kusto resource as a string """ assert callable(async_token_provider) kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.aad_federated_security] = True kcsb._async_token_provider = async_token_provider return kcsb @classmethod def with_interactive_login( cls, connection_string: str, login_hint: Optional[str] = None, domain_hint: Optional[str] = None ) -> "KustoConnectionStringBuilder": kcsb = cls(connection_string) kcsb[kcsb.ValidKeywords.interactive_login] = True kcsb[kcsb.ValidKeywords.aad_federated_security] = True if login_hint is not None: kcsb[kcsb.ValidKeywords.login_hint] = login_hint if domain_hint is not None: kcsb[kcsb.ValidKeywords.domain_hint] = domain_hint return kcsb @property def data_source(self) -> Optional[str]: """The URI specifying the Kusto service endpoint. For example, https://kuskus.kusto.windows.net or net.tcp://localhost """ return self._internal_dict.get(self.ValidKeywords.data_source) @property def aad_user_id(self) -> Optional[str]: """The username to use for AAD Federated AuthN.""" return self._internal_dict.get(self.ValidKeywords.aad_user_id) @property def password(self) -> Optional[str]: """The password to use for authentication when username/password authentication is used. Must be accompanied by UserID property """ return self._internal_dict.get(self.ValidKeywords.password) @property def application_client_id(self) -> Optional[str]: """The application client id to use for authentication when federated authentication is used. """ return self._internal_dict.get(self.ValidKeywords.application_client_id) @property def application_key(self) -> Optional[str]: """The application key to use for authentication when federated authentication is used""" return self._internal_dict.get(self.ValidKeywords.application_key) @property def application_certificate(self) -> Optional[str]: """A PEM encoded certificate private key.""" return self._internal_dict.get(self.ValidKeywords.application_certificate) @application_certificate.setter def application_certificate(self, value: str): self[self.ValidKeywords.application_certificate] = value @property def application_certificate_thumbprint(self) -> Optional[str]: """hex encoded thumbprint of the certificate.""" return self._internal_dict.get(self.ValidKeywords.application_certificate_thumbprint) @application_certificate_thumbprint.setter def application_certificate_thumbprint(self, value: str): self[self.ValidKeywords.application_certificate_thumbprint] = value @property def application_public_certificate(self) -> Optional[str]: """A public certificate matching the PEM encoded certificate private key.""" return self._internal_dict.get(self.ValidKeywords.public_application_certificate) @application_public_certificate.setter def application_public_certificate(self, value: str): self[self.ValidKeywords.public_application_certificate] = value @property def authority_id(self) -> Optional[str]: """The ID of the AAD tenant where the application is configured. (should be supplied only for non-Microsoft tenant)""" return self._internal_dict.get(self.ValidKeywords.authority_id) @authority_id.setter def authority_id(self, value: str): self[self.ValidKeywords.authority_id] = value @property def aad_federated_security(self) -> Optional[bool]: """A Boolean value that instructs the client to perform AAD federated authentication.""" return self._internal_dict.get(self.ValidKeywords.aad_federated_security) @property def user_token(self) -> Optional[str]: """User token.""" return self._internal_dict.get(self.ValidKeywords.user_token) @property def application_token(self) -> Optional[str]: """Application token.""" return self._internal_dict.get(self.ValidKeywords.application_token) @property def msi_authentication(self) -> Optional[bool]: """A value stating the MSI identity type to obtain""" return self._internal_dict.get(self.ValidKeywords.msi_auth) @property def msi_parameters(self) -> Optional[dict]: """A user assigned MSI ID to be obtained""" return self._internal_dict.get(self.ValidKeywords.msi_params) @property def az_cli(self) -> Optional[bool]: return self._internal_dict.get(self.ValidKeywords.az_cli) @property def token_provider(self) -> Optional[Callable[[], str]]: return self._token_provider @property def async_token_provider(self) -> Optional[Callable[[], Coroutine[None, None, str]]]: return self._async_token_provider @property def interactive_login(self) -> bool: val = self._internal_dict.get(self.ValidKeywords.interactive_login) return val is not None and val @property def login_hint(self) -> Optional[str]: return self._internal_dict.get(self.ValidKeywords.login_hint) @property def domain_hint(self) -> Optional[str]: return self._internal_dict.get(self.ValidKeywords.domain_hint) def __str__(self) -> str: dict_copy = self._internal_dict.copy() for key in dict_copy: if key.is_secret(): dict_copy[key] = "****" return self._build_connection_string(dict_copy) def __repr__(self) -> str: return self._build_connection_string(self._internal_dict) def _build_connection_string(self, kcsb_as_dict: dict) -> str: return ";".join(["{0}={1}".format(word.value, kcsb_as_dict[word]) for word in self.ValidKeywords if word in kcsb_as_dict]) def _assert_value_is_valid(value: str): if not value or not value.strip(): raise ValueError("Should not be empty") class ClientRequestProperties: """This class is a POD used by client making requests to describe specific needs from the service executing the requests. For more information please look at: https://docs.microsoft.com/en-us/azure/kusto/api/netfx/request-properties """ results_defer_partial_query_failures_option_name = "deferpartialqueryfailures" request_timeout_option_name = "servertimeout" no_request_timeout_option_name = "norequesttimeout" def __init__(self): self._options = {} self._parameters = {} self.client_request_id = None self.application = None self.user = None def set_parameter(self, name: str, value: str): """Sets a parameter's value""" _assert_value_is_valid(name) self._parameters[name] = value def has_parameter(self, name: str) -> bool: """Checks if a parameter is specified.""" return name in self._parameters def get_parameter(self, name: str, default_value: str) -> str: """Gets a parameter's value.""" return self._parameters.get(name, default_value) def set_option(self, name: str, value: Any): """Sets an option's value""" _assert_value_is_valid(name) self._options[name] = value def has_option(self, name: str) -> bool: """Checks if an option is specified.""" return name in self._options def get_option(self, name: str, default_value: Any) -> str: """Gets an option's value.""" return self._options.get(name, default_value) def to_json(self) -> str: """Safe serialization to a JSON string.""" return json.dumps({"Options": self._options, "Parameters": self._parameters}, default=str) class ExecuteRequestParams: def __init__(self, database: str, payload: Optional[io.IOBase], properties: ClientRequestProperties, query: str, timeout: timedelta, request_headers: dict): request_headers = copy(request_headers) json_payload = None if not payload: json_payload = {"db": database, "csl": query} if properties: json_payload["properties"] = properties.to_json() client_request_id_prefix = "KPC.execute;" request_headers["Content-Type"] = "application/json; charset=utf-8" else: if properties: request_headers.update(json.loads(properties.to_json())["Options"]) # Before 3.0 it was KPC.execute_streaming_ingest, but was changed to align with the other SDKs client_request_id_prefix = "KPC.executeStreamingIngest;" request_headers["Content-Encoding"] = "gzip" request_headers["x-ms-client-request-id"] = client_request_id_prefix + str(uuid.uuid4()) if properties is not None: if properties.client_request_id is not None: request_headers["x-ms-client-request-id"] = properties.client_request_id if properties.application is not None: request_headers["x-ms-app"] = properties.application if properties.user is not None: request_headers["x-ms-user"] = properties.user if properties.get_option(ClientRequestProperties.no_request_timeout_option_name, False): timeout = KustoClient._mgmt_default_timeout else: timeout = properties.get_option(ClientRequestProperties.request_timeout_option_name, timeout) timeout = (timeout or KustoClient._mgmt_default_timeout) + KustoClient._client_server_delta self.json_payload = json_payload self.request_headers = request_headers self.timeout = timeout class HTTPAdapterWithSocketOptions(requests.adapters.HTTPAdapter): def __init__(self, *args, **kwargs): self.socket_options = kwargs.pop("socket_options", None) self.pool_maxsize = kwargs.pop("pool_maxsize", None) self.max_retries = kwargs.pop("max_retries", None) super(HTTPAdapterWithSocketOptions, self).__init__(*args, **kwargs) def init_poolmanager(self, *args, **kwargs): if self.socket_options is not None: kwargs["socket_options"] = self.socket_options super(HTTPAdapterWithSocketOptions, self).init_poolmanager(*args, **kwargs) class _KustoClientBase(abc.ABC): API_VERSION = "2019-02-13" _mgmt_default_timeout = timedelta(hours=1, seconds=30) _query_default_timeout = timedelta(minutes=4, seconds=30) _streaming_ingest_default_timeout = timedelta(minutes=10) _aad_helper: _AadHelper def __init__(self, kcsb: Union[KustoConnectionStringBuilder, str], is_async): self._kcsb = kcsb self._proxy_url: Optional[str] = None if not isinstance(kcsb, KustoConnectionStringBuilder): self._kcsb = KustoConnectionStringBuilder(kcsb) self._kusto_cluster = self._kcsb.data_source # notice that in this context, federated actually just stands for aad auth, not aad federated auth (legacy code) self._aad_helper = _AadHelper(self._kcsb, is_async) if self._kcsb.aad_federated_security else None # Create a session object for connection pooling self._mgmt_endpoint = "{0}/v1/rest/mgmt".format(self._kusto_cluster) self._query_endpoint = "{0}/v2/rest/query".format(self._kusto_cluster) self._streaming_ingest_endpoint = "{0}/v1/rest/ingest/".format(self._kusto_cluster) self._request_headers = { "Accept": "application/json", "Accept-Encoding": "gzip,deflate", "x-ms-client-version": "Kusto.Python.Client:" + VERSION, "x-ms-version": self.API_VERSION, } def set_proxy(self, proxy_url: str): self._proxy_url = proxy_url if self._aad_helper: self._aad_helper.token_provider.set_proxy(proxy_url) @staticmethod def _kusto_parse_by_endpoint(endpoint: str, response_json: Any) -> KustoResponseDataSet: if endpoint.endswith("v2/rest/query"): return KustoResponseDataSetV2(response_json) return KustoResponseDataSetV1(response_json) @staticmethod def _handle_http_error( exception: Exception, endpoint: Optional[str], payload: Optional[io.IOBase], response: "Union[Response, aiohttp.ClientResponse]", status: int, response_json: Any, response_text: Optional[str], ) -> NoReturn: if status == 404: if payload: raise KustoServiceError("The ingestion endpoint does not exist. Please enable streaming ingestion on your cluster.", response) from exception raise KustoServiceError(f"The requested endpoint '{endpoint}' does not exist.", response) from exception if payload: message = f"An error occurred while trying to ingest: Status: {status}, Reason: {response.reason}, Text: {response_text}." if response_json: raise KustoApiError(response_json, message, response) from exception raise KustoServiceError(message, response) from exception if response_json: raise KustoApiError(response_json, http_response=response) from exception if response_text: raise KustoServiceError(response_text, response) from exception raise KustoServiceError("Server error response contains no data.", response) from exception class KustoClient(_KustoClientBase): """ Kusto client for Python. The client is a wrapper around the Kusto REST API. To read more about it, go to https://docs.microsoft.com/en-us/azure/kusto/api/rest/ The primary methods are: `execute_query`: executes a KQL query against the Kusto service. `execute_mgmt`: executes a KQL control command against the Kusto service. """ _mgmt_default_timeout = timedelta(hours=1) _query_default_timeout = timedelta(minutes=4) _streaming_ingest_default_timeout = timedelta(minutes=10) _client_server_delta = timedelta(seconds=30) # The maximum amount of connections to be able to operate in parallel _max_pool_size = 100 def __init__(self, kcsb: Union[KustoConnectionStringBuilder, str]): """ Kusto Client constructor. :param kcsb: The connection string to initialize KustoClient. :type kcsb: azure.kusto.data.KustoConnectionStringBuilder or str """ super().__init__(kcsb, False) # Create a session object for connection pooling self._session = requests.Session() adapter = HTTPAdapterWithSocketOptions( socket_options=(HTTPConnection.default_socket_options or []) + self.compose_socket_options(), pool_maxsize=self._max_pool_size ) self._session.mount("http://", adapter) self._session.mount("https://", adapter) def set_proxy(self, proxy_url: str): super().set_proxy(proxy_url) self._session.proxies = {"http": proxy_url, "https": proxy_url} def set_http_retries(self, max_retries: int): """ Set the number of HTTP retries to attempt """ adapter = HTTPAdapterWithSocketOptions( socket_options=(HTTPConnection.default_socket_options or []) + self.compose_socket_options(), pool_maxsize=self._max_pool_size, max_retries=max_retries, ) self._session.mount("http://", adapter) self._session.mount("https://", adapter) @staticmethod def compose_socket_options() -> List[Tuple[int, int, int]]: # Sends TCP Keep-Alive after MAX_IDLE_SECONDS seconds of idleness, once every INTERVAL_SECONDS seconds, and closes the connection after MAX_FAILED_KEEPALIVES failed pings (e.g. 20 => 1:00:30) MAX_IDLE_SECONDS = 30 INTERVAL_SECONDS = 180 # Corresponds to Azure Load Balancer Service 4 minute timeout, with 1 minute of slack MAX_FAILED_KEEPALIVES = 20 if ( sys.platform == "linux" and hasattr(socket, "SOL_SOCKET") and hasattr(socket, "SO_KEEPALIVE") and hasattr(socket, "TCP_KEEPIDLE") and hasattr(socket, "TCP_KEEPINTVL") and hasattr(socket, "TCP_KEEPCNT") ): return [ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, MAX_IDLE_SECONDS), (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, INTERVAL_SECONDS), (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, MAX_FAILED_KEEPALIVES), ] elif ( sys.platform == "win32" and hasattr(socket, "SOL_SOCKET") and hasattr(socket, "SO_KEEPALIVE") and hasattr(socket, "TCP_KEEPIDLE") and hasattr(socket, "TCP_KEEPCNT") ): return [ (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, MAX_IDLE_SECONDS), (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, MAX_FAILED_KEEPALIVES), ] elif sys.platform == "darwin" and hasattr(socket, "SOL_SOCKET") and hasattr(socket, "SO_KEEPALIVE") and hasattr(socket, "IPPROTO_TCP"): TCP_KEEPALIVE = 0x10 return [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), (socket.IPPROTO_TCP, TCP_KEEPALIVE, INTERVAL_SECONDS)] else: return [] def execute(self, database: str, query: str, properties: Optional[ClientRequestProperties] = None) -> KustoResponseDataSet: """ Executes a query or management command. :param str database: Database against query will be executed. :param str query: Query to be executed. :param azure.kusto.data.ClientRequestProperties properties: Optional additional properties. :return: Kusto response data set. :rtype: azure.kusto.data.response.KustoResponseDataSet """ query = query.strip() if query.startswith("."): return self.execute_mgmt(database, query, properties) return self.execute_query(database, query, properties) def execute_query(self, database: str, query: str, properties: Optional[ClientRequestProperties] = None) -> KustoResponseDataSet: """ Execute a KQL query. To learn more about KQL go to https://docs.microsoft.com/en-us/azure/kusto/query/ :param str database: Database against query will be executed. :param str query: Query to be executed. :param azure.kusto.data.ClientRequestProperties properties: Optional additional properties. :return: Kusto response data set. :rtype: azure.kusto.data.response.KustoResponseDataSet """ return self._execute(self._query_endpoint, database, query, None, self._query_default_timeout, properties) def execute_mgmt(self, database: str, query: str, properties: Optional[ClientRequestProperties] = None) -> KustoResponseDataSet: """ Execute a KQL control command. To learn more about KQL control commands go to https://docs.microsoft.com/en-us/azure/kusto/management/ :param str database: Database against query will be executed. :param str query: Query to be executed. :param azure.kusto.data.ClientRequestProperties properties: Optional additional properties. :return: Kusto response data set. :rtype: azure.kusto.data.response.KustoResponseDataSet """ return self._execute(self._mgmt_endpoint, database, query, None, self._mgmt_default_timeout, properties) def execute_streaming_ingest( self, database: str, table: str, stream: IO[AnyStr], stream_format: Union[DataFormat, str], properties: Optional[ClientRequestProperties] = None, mapping_name: str = None, ): """ Execute streaming ingest against this client If the Kusto service is not configured to allow streaming ingestion, this may raise an error To learn more about streaming ingestion go to: https://docs.microsoft.com/en-us/azure/data-explorer/ingest-data-streaming :param str database: Target database. :param str table: Target table. :param io.BaseIO stream: stream object which contains the data to ingest. :param DataFormat stream_format: Format of the data in the stream. :param ClientRequestProperties properties: additional request properties. :param str mapping_name: Pre-defined mapping of the table. Required when stream_format is json/avro. """ stream_format = stream_format.kusto_value if isinstance(stream_format, DataFormat) else DataFormat[stream_format.upper()].kusto_value endpoint = self._streaming_ingest_endpoint + database + "/" + table + "?streamFormat=" + stream_format if mapping_name is not None: endpoint = endpoint + "&mappingName=" + mapping_name self._execute(endpoint, database, None, stream, self._streaming_ingest_default_timeout, properties) def _execute_streaming_query_parsed( self, database: str, query: str, timeout: timedelta = _KustoClientBase._query_default_timeout, properties: Optional[ClientRequestProperties] = None ) -> StreamingDataSetEnumerator: response = self._execute(self._query_endpoint, database, query, None, timeout, properties, stream_response=True) response.raw.decode_content = True return StreamingDataSetEnumerator(JsonTokenReader(response.raw)) def execute_streaming_query( self, database: str, query: str, timeout: timedelta = _KustoClientBase._query_default_timeout, properties: Optional[ClientRequestProperties] = None ) -> KustoStreamingResponseDataSet: """ Execute a KQL query without reading it all to memory. The resulting KustoStreamingResponseDataSet will stream one table at a time, and the rows can be retrieved sequentially. :param str database: Database against query will be executed. :param str query: Query to be executed. :param timedelta timeout: timeout for the query to be executed :param azure.kusto.data.ClientRequestProperties properties: Optional additional properties. :return KustoStreamingResponseDataSet: """ return KustoStreamingResponseDataSet(self._execute_streaming_query_parsed(database, query, timeout, properties)) def _execute( self, endpoint: str, database: str, query: Optional[str], payload: Optional[IO[AnyStr]], timeout: timedelta, properties: Optional[ClientRequestProperties] = None, stream_response: bool = False, ) -> Union[KustoResponseDataSet, Response]: """Executes given query against this client""" request_params = ExecuteRequestParams(database, payload, properties, query, timeout, self._request_headers) json_payload = request_params.json_payload request_headers = request_params.request_headers timeout = request_params.timeout if self._aad_helper: request_headers["Authorization"] = self._aad_helper.acquire_authorization_header() response = self._session.post(endpoint, headers=request_headers, json=json_payload, data=payload, timeout=timeout.seconds, stream=stream_response) if stream_response: try: response.raise_for_status() return response except Exception as e: raise self._handle_http_error(e, self._query_endpoint, None, response, response.status_code, response.json(), response.text) response_json = None try: response_json = response.json() response.raise_for_status() except Exception as e: raise self._handle_http_error(e, endpoint, payload, response, response.status_code, response_json, response.text) return self._kusto_parse_by_endpoint(endpoint, response_json) azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/data_format.py000066400000000000000000000036261417222763000265750ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License from enum import Enum class IngestionMappingKind(Enum): CSV = "Csv" JSON = "Json" AVRO = "Avro" APACHEAVRO = "ApacheAvro" PARQUET = "Parquet" SSTREAM = "SStream" ORC = "Orc" W3CLOGFILE = "W3CLogFile" UNKNOWN = "Unknown" class DataFormat(Enum): """All data formats supported by Kusto.""" CSV = ("csv", IngestionMappingKind.CSV, False, True) TSV = ("tsv", IngestionMappingKind.CSV, False, True) SCSV = ("scsv", IngestionMappingKind.CSV, False, True) SOHSV = ("sohsv", IngestionMappingKind.CSV, False, True) PSV = ("psv", IngestionMappingKind.CSV, False, True) TXT = ("txt", IngestionMappingKind.CSV, False, True) TSVE = ("tsve", IngestionMappingKind.CSV, False, True) JSON = ("json", IngestionMappingKind.JSON, True, True) SINGLEJSON = ("singlejson", IngestionMappingKind.JSON, True, True) MULTIJSON = ("multijson", IngestionMappingKind.JSON, True, True) AVRO = ("avro", IngestionMappingKind.AVRO, True, True) APACHEAVRO = ("apacheavro", IngestionMappingKind.APACHEAVRO, False, True) PARQUET = ("parquet", IngestionMappingKind.PARQUET, False, False) SSTREAM = ("sstream", IngestionMappingKind.SSTREAM, False, False) ORC = ("orc", IngestionMappingKind.ORC, False, False) RAW = ("raw", IngestionMappingKind.CSV, False, True) W3CLOGFILE = ("w3clogfile", IngestionMappingKind.W3CLOGFILE, False, True) def __init__(self, kusto_value: str, ingestion_mapping_kind: IngestionMappingKind, mapping_required: bool, compressible: bool): self.kusto_value = kusto_value # Formatted how Kusto Service expects it self.ingestion_mapping_kind = ingestion_mapping_kind # Deprecated - will probably be removed soon self._mapping_required = mapping_required self.compressible = compressible # Binary formats should not be compressed azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/exceptions.py000066400000000000000000000133571417222763000264770ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License from typing import List, Union, TYPE_CHECKING, Optional, Dict, Any if TYPE_CHECKING: import requests try: from aiohttp import ClientResponse except ImportError: # No aio installed, ignore ClientResponse = None pass class KustoError(Exception): """Base class for all exceptions raised by the Kusto Python Client Libraries.""" class KustoStreamingQueryError(KustoError): ... class KustoTokenParsingError(KustoStreamingQueryError): ... class KustoServiceError(KustoError): """Raised when the Kusto service was unable to process a request.""" def __init__( self, messages: Union[str, List[dict]], http_response: "Union[requests.Response, ClientResponse, None]" = None, kusto_response: Optional[Dict[str, Any]] = None, ): super().__init__(messages) self.http_response = http_response self.kusto_response = kusto_response def get_raw_http_response(self) -> "Union[requests.Response, ClientResponse, None]": """Gets the http response.""" return self.http_response def is_semantic_error(self) -> bool: """Checks if a response is a semantic error.""" try: return "Semantic error:" in self.http_response.text except AttributeError: return False def has_partial_results(self) -> bool: """Checks if a response exists.""" return self.kusto_response is not None def get_partial_results(self) -> Optional[Dict[str, Any]]: """Gets the Kusto response.""" return self.kusto_response class OneApiError: def __init__(self, code: str, message: str, type: str, description: str, context: dict, permanent: bool) -> None: self.code = code self.message = message self.type = type self.description = description self.context = context self.permanent = permanent @staticmethod def from_dict(obj: dict) -> "OneApiError": code = obj["code"] message = obj["message"] type = obj["@type"] description = obj["@message"] context = obj["@context"] permanent = obj["@permanent"] return OneApiError(code, message, type, description, context, permanent) class KustoMultiApiError(KustoServiceError): """ Represents a collection of standard API errors from kusto. Use `get_api_errors()` to retrieve more details. """ def __init__(self, errors: List[dict]): self.errors = KustoMultiApiError.parse_errors(errors) messages = [error.description for error in self.errors] super().__init__(messages[0] if len(self.errors) == 1 else messages) def get_api_errors(self) -> List[OneApiError]: return self.errors @staticmethod def parse_errors(errors: List[dict]) -> List[OneApiError]: parsed_errors = [] for error_block in errors: one_api_errors = error_block.get("OneApiErrors", None) if not one_api_errors: continue for inner_error in one_api_errors: error_dict = inner_error.get("error", None) if error_dict: parsed_errors.append(OneApiError.from_dict(error_dict)) return parsed_errors class KustoApiError(KustoServiceError): """ Represents a standard API error from kusto. Use `get_api_error()` to retrieve more details. """ def __init__(self, error_dict: dict, message: str = None, http_response: "Union[requests.Response, ClientResponse, None]" = None, kusto_response=None): self.error = OneApiError.from_dict(error_dict["error"]) super().__init__(message or self.error.description, http_response, kusto_response) def get_api_error(self) -> OneApiError: return self.error class KustoClientError(KustoError): """Raised when a Kusto client is unable to send or complete a request.""" class KustoBlobError(KustoClientError): def __init__(self, inner: Exception): self.inner = inner def message(self) -> str: return f"Failed to upload blob: {self.inner}" class KustoUnsupportedApiError(KustoError): """Raised when a Kusto client is unable to send or complete a request.""" @staticmethod def progressive_api_unsupported() -> "KustoUnsupportedApiError": return KustoUnsupportedApiError("Progressive API is unsupported - to resolve, set results_progressive_enabled=false") class KustoAuthenticationError(KustoClientError): """Raised when authentication fails.""" def __init__(self, authentication_method: str, exception: Exception, **kwargs): super().__init__() self.authentication_method = authentication_method self.exception = exception if "authority" in kwargs: self.authority = kwargs["authority"] if "kusto_uri" in kwargs: self.kusto_cluster = kwargs["kusto_uri"] self.kwargs = kwargs def __str__(self): return repr(self) def __repr__(self): return "KustoAuthenticationError('{}', '{}', '{}')".format(self.authentication_method, repr(self.exception), self.kwargs) class KustoAioSyntaxError(SyntaxError): """Raised when trying to use aio syntax without installing the needed modules""" def __init__(self): super().__init__("Aio modules not installed, run 'pip install azure-kusto-data[aio]' to leverage aio capabilities") class KustoAsyncUsageError(Exception): """Raised when trying to use async methods on a sync object, and vice-versa""" def __init__(self, method: str, is_client_async: bool): super().__init__("Method {} can't be called from {} client".format(method, "an asynchronous" if is_client_async else "a synchronous")) azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/helpers.py000066400000000000000000000054431417222763000257550ustar00rootroot00000000000000from typing import TYPE_CHECKING, Union import numpy as np if TYPE_CHECKING: import pandas from azure.kusto.data._models import KustoResultTable, KustoStreamingResultTable # Copyright (c) Microsoft Corporation. # Licensed under the MIT License def to_pandas_timedelta(raw_value: Union[int, float, str]) -> "pandas.Timedelta": """ Transform a raw python value to a pandas timedelta. """ import pandas as pd if isinstance(raw_value, (int, float)): # https://docs.microsoft.com/en-us/dotnet/api/system.datetime.ticks # Kusto saves up to ticks, 1 tick == 100 nanoseconds return pd.to_timedelta(raw_value * 100, unit="ns") if isinstance(raw_value, str): # The timespan format Kusto returns is 'd.hh:mm:ss.ssssss' or 'hh:mm:ss.ssssss' or 'hh:mm:ss' # Pandas expects 'd days hh:mm:ss.ssssss' or 'hh:mm:ss.ssssss' or 'hh:mm:ss' parts = raw_value.split(":") if "." not in parts[0]: return pd.to_timedelta(raw_value) else: formatted_value = raw_value.replace(".", " days ", 1) return pd.to_timedelta(formatted_value) def dataframe_from_result_table(table: "Union[KustoResultTable, KustoStreamingResultTable]") -> "pandas.DataFrame": """Converts Kusto tables into pandas DataFrame. :param azure.kusto.data._models.KustoResultTable table: Table received from the response. :return: pandas DataFrame. """ import pandas as pd if not table: raise ValueError() from azure.kusto.data._models import KustoResultTable, KustoStreamingResultTable if not isinstance(table, KustoResultTable) and not isinstance(table, KustoStreamingResultTable): raise TypeError("Expected KustoResultTable or KustoStreamingResultTable got {}".format(type(table).__name__)) columns = [col.column_name for col in table.columns] frame = pd.DataFrame(table.raw_rows, columns=columns) # fix types for col in table.columns: if col.column_type == "bool": frame[col.column_name] = frame[col.column_name].astype(bool) elif col.column_type == "int" or col.column_type == "long": frame[col.column_name] = frame[col.column_name].astype("Int64") elif col.column_type == "real" or col.column_type == "decimal": frame[col.column_name] = frame[col.column_name].replace("NaN", np.NaN).replace("Infinity", np.PINF).replace("-Infinity", np.NINF) frame[col.column_name] = pd.to_numeric(frame[col.column_name], errors="coerce").astype("Float64") elif col.column_type == "datetime": frame[col.column_name] = pd.to_datetime(frame[col.column_name], errors="coerce") elif col.column_type == "timespan": frame[col.column_name] = frame[col.column_name].apply(to_pandas_timedelta) return frame azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/response.py000066400000000000000000000212551417222763000261500ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License from abc import ABCMeta, abstractmethod from typing import List, Iterator, Union, Dict, Any from ._models import KustoResultTable, WellKnownDataSet, KustoStreamingResultTable, BaseKustoResultTable from .exceptions import KustoStreamingQueryError from .streaming_response import StreamingDataSetEnumerator, FrameType class BaseKustoResponseDataSet(metaclass=ABCMeta): tables: list tables_count: int tables_names: list @property @abstractmethod def _error_column(self) -> str: raise NotImplementedError @property @abstractmethod def _crid_column(self) -> str: raise NotImplementedError @property @abstractmethod def _status_column(self) -> str: raise NotImplementedError @property def errors_count(self) -> int: """Checks whether an exception was thrown.""" query_status_table = next((t for t in self.tables if t.table_kind == WellKnownDataSet.QueryCompletionInformation), None) if not query_status_table: return 0 min_level = 4 errors = 0 for row in query_status_table: if row[self._error_column] < 4: if row[self._error_column] < min_level: min_level = row[self._error_column] errors = 1 elif row[self._error_column] == min_level: errors += 1 return errors def get_exceptions(self) -> List[str]: """Gets the exceptions retrieved from Kusto if exists.""" query_status_table = next((t for t in self.tables if t.table_kind == WellKnownDataSet.QueryCompletionInformation), None) if not query_status_table: return [] result = [] for row in query_status_table: if row[self._error_column] < 4: result.append( "Please provide the following data to Kusto: CRID='{0}' Description:'{1}'".format(row[self._crid_column], row[self._status_column]) ) return result def __iter__(self) -> Iterator[BaseKustoResultTable]: return iter(self.tables) def __getitem__(self, key: Union[int, str]) -> KustoResultTable: if isinstance(key, int): return self.tables[key] try: return self.tables[self.tables_names.index(key)] except ValueError: raise LookupError(key) def __len__(self) -> int: return self.tables_count class KustoResponseDataSet(BaseKustoResponseDataSet, metaclass=ABCMeta): """ `KustoResponseDataSet` Represents the parsed data set carried by the response to a Kusto request. `KustoResponseDataSet` provides convenient methods to work with the returned result. The result table(s) are accessable via the @primary_results property. @primary_results returns a collection of `KustoResultTable`. It can contain more than one table when [`fork`](https://docs.microsoft.com/en-us/azure/kusto/query/forkoperator) is used. """ def __init__(self, json_response: List[Dict[str, Any]]): self.tables = [KustoResultTable(t) for t in json_response] self.tables_count = len(self.tables) self.tables_names = [t.table_name for t in self.tables] @property def primary_results(self) -> List[KustoResultTable]: """Returns primary results. If there is more than one returns a list.""" if self.tables_count == 1: return self.tables primary = [x for x in self.tables if x.table_kind == WellKnownDataSet.PrimaryResult] return primary def __iter__(self) -> Iterator[KustoResultTable]: return iter(self.tables) class KustoResponseDataSetV1(KustoResponseDataSet): """ KustoResponseDataSetV1 is a wrapper for a V1 Kusto response. It parses V1 response into a convenient KustoResponseDataSet. To read more about V1 response structure, please check out https://docs.microsoft.com/en-us/azure/kusto/api/rest/response """ _status_column = "StatusDescription" _crid_column = "ClientActivityId" _error_column = "Severity" _tables_kinds = { "QueryResult": WellKnownDataSet.PrimaryResult, "QueryProperties": WellKnownDataSet.QueryProperties, "QueryStatus": WellKnownDataSet.QueryCompletionInformation, } def __init__(self, json_response: dict): super(KustoResponseDataSetV1, self).__init__(json_response["Tables"]) if self.tables_count <= 2: self.tables[0].table_kind = WellKnownDataSet.PrimaryResult self.tables[0].table_id = 0 if self.tables_count == 2: self.tables[1].table_kind = WellKnownDataSet.QueryProperties self.tables[1].table_id = 1 else: toc = self.tables[-1] toc.table_kind = WellKnownDataSet.TableOfContents toc.table_id = self.tables_count - 1 for i in range(self.tables_count - 1): self.tables[i].table_name = toc[i]["Name"] self.tables[i].table_id = toc[i]["Id"] self.tables[i].table_kind = self._tables_kinds[toc[i]["Kind"]] class KustoResponseDataSetV2(KustoResponseDataSet): """ KustoResponseDataSetV2 is a wrapper for a V2 Kusto response. It parses V2 response into a convenient KustoResponseDataSet. To read more about V2 response structure, please check out https://docs.microsoft.com/en-us/azure/kusto/api/rest/response2 """ _status_column = "Payload" _error_column = "Level" _crid_column = "ClientRequestId" def __init__(self, json_response: List[dict]): super(KustoResponseDataSetV2, self).__init__([t for t in json_response if t["FrameType"] == "DataTable"]) class KustoStreamingResponseDataSet(BaseKustoResponseDataSet): _status_column = "Payload" _error_column = "Level" _crid_column = "ClientRequestId" def __init__(self, streamed_data: StreamingDataSetEnumerator): self._current_table = None self._skip_incomplete_tables = False self.tables = [] self.streamed_data = streamed_data self.finished = False def iter_primary_results(self) -> "PrimaryResultsIterator": return PrimaryResultsIterator(self) def __iter__(self) -> Iterator[Union[KustoResultTable, KustoStreamingResultTable]]: return self def __next__(self) -> Union[KustoResultTable, KustoStreamingResultTable]: if self.finished: raise StopIteration if type(self._current_table) is KustoStreamingResultTable and not self._current_table.finished and not self._skip_incomplete_tables: raise KustoStreamingQueryError( "Tried retrieving a new primary_result table before the old one was finished. To override call `set_skip_incomplete_tables(True)`" ) while True: try: table = next(self.streamed_data) except StopIteration: self.finished = True raise if table["FrameType"] == FrameType.DataTable: break if table["TableKind"] == WellKnownDataSet.PrimaryResult.value: self._current_table = KustoStreamingResultTable(table) else: self._current_table = KustoResultTable(table) self.tables.append(self._current_table) return self._current_table def set_skip_incomplete_tables(self, value: bool): self._skip_incomplete_tables = value @property def errors_count(self) -> int: if not self.finished: raise KustoStreamingQueryError("Unable to get errors count before reading all of the tables.") return super().errors_count def get_exceptions(self) -> List[str]: if not self.finished: raise KustoStreamingQueryError("Unable to get errors count before reading all of the tables.") return super().get_exceptions() def __getitem__(self, key) -> KustoResultTable: if isinstance(key, int): return self.tables[key] try: return next(t for t in self.tables if t.table_name == key) except StopIteration: raise LookupError(key) def __len__(self) -> int: return len(self.tables) class PrimaryResultsIterator: # This class exists because you can't raise exception from an generator and keep working def __init__(self, dataset: KustoStreamingResponseDataSet): self.dataset = dataset def __iter__(self) -> Iterator[KustoStreamingResultTable]: return self def __next__(self) -> KustoStreamingResultTable: while True: table = next(self.dataset) if type(table) is KustoStreamingResultTable: return table azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/security.py000066400000000000000000000105031417222763000261530ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License from typing import Optional, Dict, TYPE_CHECKING from urllib.parse import urlparse from ._token_providers import ( TokenProviderBase, BasicTokenProvider, CallbackTokenProvider, MsiTokenProvider, AzCliTokenProvider, UserPassTokenProvider, DeviceLoginTokenProvider, InteractiveLoginTokenProvider, ApplicationKeyTokenProvider, ApplicationCertificateTokenProvider, TokenConstants, ) from .exceptions import KustoAuthenticationError, KustoClientError if TYPE_CHECKING: from . import KustoConnectionStringBuilder class _AadHelper: kusto_uri = None # type: str authority_uri = None # type: str token_provider = None # type: TokenProviderBase def __init__(self, kcsb: "KustoConnectionStringBuilder", is_async: bool): self.kusto_uri = "{0.scheme}://{0.hostname}".format(urlparse(kcsb.data_source)) self.username = None if kcsb.interactive_login: self.token_provider = InteractiveLoginTokenProvider(self.kusto_uri, kcsb.authority_id, kcsb.login_hint, kcsb.domain_hint, is_async=is_async) elif all([kcsb.aad_user_id, kcsb.password]): self.token_provider = UserPassTokenProvider(self.kusto_uri, kcsb.authority_id, kcsb.aad_user_id, kcsb.password, is_async=is_async) elif all([kcsb.application_client_id, kcsb.application_key]): self.token_provider = ApplicationKeyTokenProvider( self.kusto_uri, kcsb.authority_id, kcsb.application_client_id, kcsb.application_key, is_async=is_async ) elif all([kcsb.application_client_id, kcsb.application_certificate, kcsb.application_certificate_thumbprint]): # kcsb.application_public_certificate can be None if SNI is not used self.token_provider = ApplicationCertificateTokenProvider( self.kusto_uri, kcsb.application_client_id, kcsb.authority_id, kcsb.application_certificate, kcsb.application_certificate_thumbprint, kcsb.application_public_certificate, is_async=is_async, ) elif kcsb.msi_authentication: self.token_provider = MsiTokenProvider(self.kusto_uri, kcsb.msi_parameters, is_async=is_async) elif kcsb.user_token: self.token_provider = BasicTokenProvider(kcsb.user_token, is_async=is_async) elif kcsb.application_token: self.token_provider = BasicTokenProvider(kcsb.application_token, is_async=is_async) elif kcsb.az_cli: self.token_provider = AzCliTokenProvider(self.kusto_uri, is_async=is_async) elif kcsb.token_provider or kcsb.async_token_provider: self.token_provider = CallbackTokenProvider(token_callback=kcsb.token_provider, async_token_callback=kcsb.async_token_provider, is_async=is_async) else: self.token_provider = DeviceLoginTokenProvider(self.kusto_uri, kcsb.authority_id, is_async=is_async) def acquire_authorization_header(self): try: return _get_header_from_dict(self.token_provider.get_token()) except Exception as error: kwargs = self.token_provider.context() kwargs["kusto_uri"] = self.kusto_uri raise KustoAuthenticationError(self.token_provider.name(), error, **kwargs) async def acquire_authorization_header_async(self): try: return _get_header_from_dict(await self.token_provider.get_token_async()) except Exception as error: kwargs = await self.token_provider.context_async() kwargs["resource"] = self.kusto_uri raise KustoAuthenticationError(self.token_provider.name(), error, **kwargs) def _get_header_from_dict(token: dict): if TokenConstants.MSAL_ACCESS_TOKEN in token: return _get_header(token[TokenConstants.MSAL_TOKEN_TYPE], token[TokenConstants.MSAL_ACCESS_TOKEN]) elif TokenConstants.AZ_ACCESS_TOKEN in token: return _get_header(token[TokenConstants.AZ_TOKEN_TYPE], token[TokenConstants.AZ_ACCESS_TOKEN]) else: raise KustoClientError("Unable to determine the token type. Neither 'tokenType' nor 'token_type' property is present.") def _get_header(token_type: str, access_token: str) -> str: return "{0} {1}".format(token_type, access_token) azure-kusto-python-3.0.1/azure-kusto-data/azure/kusto/data/streaming_response.py000066400000000000000000000247621417222763000302270ustar00rootroot00000000000000from enum import Enum from typing import Optional, Any, Tuple, Dict, AnyStr, IO, List, Iterator import ijson from ijson import IncompleteJSONError from azure.kusto.data._models import WellKnownDataSet from azure.kusto.data.exceptions import KustoServiceError, KustoTokenParsingError, KustoUnsupportedApiError, KustoApiError, KustoMultiApiError class JsonTokenType(Enum): NULL = 0 BOOLEAN = 1 NUMBER = 2 STRING = 3 MAP_KEY = 4 START_MAP = 5 END_MAP = 6 START_ARRAY = 7 END_ARRAY = 8 @staticmethod def start_tokens() -> "List[JsonTokenType]": return [JsonTokenType.START_MAP, JsonTokenType.START_ARRAY] @staticmethod def end_tokens() -> "List[JsonTokenType]": return [JsonTokenType.END_MAP, JsonTokenType.END_ARRAY] class FrameType(Enum): DataSetHeader = 0 TableHeader = 1 TableFragment = 2 TableCompletion = 3 TableProgress = 4 DataTable = 5 DataSetCompletion = 6 class JsonToken: def __init__(self, token_path: str, token_type: JsonTokenType, token_value: Optional[Any]): self.token_path = token_path self.token_type = token_type self.token_value = token_value class JsonTokenReader: def __init__(self, stream: IO[AnyStr]): self.json_iter = ijson.parse(stream, use_float=True) def __iter__(self) -> "JsonTokenReader": return self def __next__(self) -> JsonToken: return self.read_next_token_or_throw() def read_next_token_or_throw(self) -> JsonToken: try: next_item = next(self.json_iter) except IncompleteJSONError: next_item = None if next_item is None: raise KustoTokenParsingError("Unexpected end of stream") (token_path, token_type, token_value) = next_item return JsonToken(token_path, JsonTokenType[token_type.upper()], token_value) def read_token_of_type(self, *token_types: JsonTokenType) -> JsonToken: token = self.read_next_token_or_throw() if token.token_type not in token_types: raise KustoTokenParsingError(f"Expected one the following types: '{','.join(t.name for t in token_types)}' , got type {token.token_type}") return token def read_start_object(self) -> JsonToken: return self.read_token_of_type(JsonTokenType.START_MAP) def read_start_array(self) -> JsonToken: return self.read_token_of_type(JsonTokenType.START_ARRAY) def read_string(self) -> str: return self.read_token_of_type(JsonTokenType.STRING).token_value def read_boolean(self) -> bool: return self.read_token_of_type(JsonTokenType.BOOLEAN).token_value def read_number(self) -> float: return self.read_token_of_type(JsonTokenType.NUMBER).token_value def skip_children(self, prev_token: JsonToken): if prev_token.token_type == JsonTokenType.MAP_KEY: prev_token = self.read_next_token_or_throw() if prev_token.token_type in JsonTokenType.start_tokens(): for potential_end_token in self: if potential_end_token.token_path == prev_token.token_path and potential_end_token.token_type in JsonTokenType.end_tokens(): break def skip_until_property_name(self, name: str) -> JsonToken: while True: token = self.read_token_of_type(JsonTokenType.MAP_KEY) if token.token_value == name: return token self.skip_children(token) def skip_until_any_property_name(self, *names: str) -> JsonToken: while True: token = self.read_token_of_type(JsonTokenType.MAP_KEY) if token.token_value in names: return token self.skip_children(token) def skip_until_property_name_or_end_object(self, *names: str) -> JsonToken: for token in self: if token.token_type == JsonTokenType.END_MAP: return token if token.token_type == JsonTokenType.MAP_KEY: if token.token_value in names: return token self.skip_children(token) continue raise Exception(f"Unexpected token {token}") def skip_until_token_with_paths(self, *tokens: (JsonTokenType, str)) -> JsonToken: for token in self: if any((token.token_type == t_type and token.token_path == t_path) for (t_type, t_path) in tokens): return token self.skip_children(token) class StreamingDataSetEnumerator: def __init__(self, reader: JsonTokenReader): self.reader = reader self.done = False self.started = False self.started_primary_results = False self.finished_primary_results = False def __iter__(self) -> "StreamingDataSetEnumerator": return self def __next__(self) -> Dict[str, Any]: if self.done: raise StopIteration() if not self.started: self.reader.read_start_array() self.started = True token = self.reader.skip_until_token_with_paths((JsonTokenType.START_MAP, "item"), (JsonTokenType.END_ARRAY, "")) if token == JsonTokenType.END_ARRAY: self.done = True raise StopIteration() frame_type = self.read_frame_type() parsed_frame = self.parse_frame(frame_type) is_primary_result = parsed_frame["FrameType"] == FrameType.DataTable and parsed_frame["TableKind"] == WellKnownDataSet.PrimaryResult.value if is_primary_result: self.started_primary_results = True elif self.started_primary_results: self.finished_primary_results = True return parsed_frame def parse_frame(self, frame_type: FrameType) -> Dict[str, Any]: if frame_type == FrameType.DataSetHeader: frame = self.extract_props(frame_type, ("IsProgressive", JsonTokenType.BOOLEAN), ("Version", JsonTokenType.STRING)) if frame["IsProgressive"]: raise KustoUnsupportedApiError.progressive_api_unsupported() return frame if frame_type in [FrameType.TableHeader, FrameType.TableFragment, FrameType.TableCompletion, FrameType.TableProgress]: raise KustoUnsupportedApiError.progressive_api_unsupported() if frame_type == FrameType.DataTable: props = self.extract_props( frame_type, ("TableId", JsonTokenType.NUMBER), ("TableKind", JsonTokenType.STRING), ("TableName", JsonTokenType.STRING), ("Columns", JsonTokenType.START_ARRAY), ) self.reader.skip_until_property_name("Rows") props["Rows"] = self.row_iterator() if props["TableKind"] != WellKnownDataSet.PrimaryResult.value: props["Rows"] = list(props["Rows"]) return props if frame_type == FrameType.DataSetCompletion: res = self.extract_props(frame_type, ("HasErrors", JsonTokenType.BOOLEAN), ("Cancelled", JsonTokenType.BOOLEAN)) token = self.reader.skip_until_property_name_or_end_object("OneApiErrors") if token.token_type != JsonTokenType.END_MAP: res["OneApiErrors"] = self.parse_array(skip_start=False) return res def row_iterator(self) -> Iterator[list]: self.reader.read_token_of_type(JsonTokenType.START_ARRAY) while True: token = self.reader.read_token_of_type(JsonTokenType.START_ARRAY, JsonTokenType.END_ARRAY, JsonTokenType.START_MAP) if token.token_type == JsonTokenType.START_MAP: # Todo - this method of error handling may be problematic, since after raising an error the iteration stops. # This means that if there are more data or even more errors, we can't read them raise KustoMultiApiError([self.parse_object(skip_start=True)]) if token.token_type == JsonTokenType.END_ARRAY: return yield self.parse_array(skip_start=True) def parse_array(self, skip_start: bool) -> list: if not skip_start: self.reader.read_start_array() arr = [] while True: token = self.reader.read_token_of_type( JsonTokenType.NULL, JsonTokenType.BOOLEAN, JsonTokenType.NUMBER, JsonTokenType.STRING, JsonTokenType.START_MAP, JsonTokenType.START_ARRAY, JsonTokenType.END_ARRAY, ) if token.token_type == JsonTokenType.END_ARRAY: return arr if token.token_type == JsonTokenType.START_MAP: arr.append(self.parse_object(skip_start=True)) elif token.token_type == JsonTokenType.START_ARRAY: arr.append(self.parse_array(skip_start=True)) else: arr.append(token.token_value) def parse_object(self, skip_start: bool) -> Dict[str, Any]: if not skip_start: self.reader.read_start_object() obj = {} while True: token_prop_name = self.reader.read_token_of_type(JsonTokenType.MAP_KEY, JsonTokenType.END_MAP) if token_prop_name.token_type == JsonTokenType.END_MAP: return obj prop_name = token_prop_name.token_value token = self.reader.read_token_of_type( JsonTokenType.NULL, JsonTokenType.BOOLEAN, JsonTokenType.NUMBER, JsonTokenType.STRING, JsonTokenType.START_MAP, JsonTokenType.START_ARRAY ) if token.token_type == JsonTokenType.START_MAP: obj[prop_name] = self.parse_object(skip_start=True) elif token.token_type == JsonTokenType.START_ARRAY: obj[prop_name] = self.parse_array(skip_start=True) else: obj[prop_name] = token.token_value def extract_props(self, frame_type: FrameType, *props: Tuple[str, JsonTokenType]) -> Dict[str, Any]: result = {"FrameType": frame_type} props_dict = dict(props) while props_dict: name = self.reader.skip_until_any_property_name(*props_dict.keys()).token_value if props_dict[name] == JsonTokenType.START_ARRAY: result[name] = self.parse_array(skip_start=False) else: result[name] = self.reader.read_token_of_type(props_dict[name]).token_value props_dict.pop(name) return result def read_frame_type(self) -> FrameType: self.reader.skip_until_property_name("FrameType") return FrameType[self.reader.read_string()] azure-kusto-python-3.0.1/azure-kusto-data/setup.cfg000066400000000000000000000002271417222763000223510ustar00rootroot00000000000000[bdist_wheel] universal=1 [flake8] ignore = E226,E302,E41 max-line-length = 160 exclude = tests/* max-complexity = 10 [pylint] max-line-length = 160azure-kusto-python-3.0.1/azure-kusto-data/setup.py000066400000000000000000000033041417222763000222410ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import re from os import path # Always prefer setuptools over distutils from setuptools import setup, find_packages PACKAGE_NAME = "azure-kusto-data" # a-b-c => a/b/c PACKAGE_FOLDER_PATH = PACKAGE_NAME.replace("-", path.sep) # a-b-c => a.b.c NAMESPACE_NAME = PACKAGE_NAME.replace("-", ".") with open(path.join(PACKAGE_FOLDER_PATH, "_version.py"), "r") as fd: VERSION = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) if not VERSION: raise RuntimeError("Cannot find version information") setup( name=PACKAGE_NAME, version=VERSION, description="Kusto Data Client", long_description=open("README.rst", "r").read(), url="https://github.com/Azure/azure-kusto-python", author="Microsoft Corporation", author_email="kustalk@microsoft.com", license="MIT", classifiers=[ # 5 - Production/Stable depends on multi-threading / aio / perf "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Topic :: Software Development", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "License :: OSI Approved :: MIT License", ], namespace_packages=["azure"], keywords="kusto wrapper client library", packages=find_packages(exclude=["azure", "tests"]), install_requires=["python-dateutil>=2.8.0", "requests>=2.13.0", "azure-identity>=1.5.0,<2", "msal>=1.9.0,<2", "ijson~=3.1"], extras_require={"pandas": ["pandas"], "aio": ["aiohttp>=3.4.4,<4", "asgiref>=3.2.3,<4"]}, ) azure-kusto-python-3.0.1/azure-kusto-data/tests/000077500000000000000000000000001417222763000216715ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/tests/__init__.py000066400000000000000000000000001417222763000237700ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/tests/aio/000077500000000000000000000000001417222763000224415ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/tests/aio/__init__.py000066400000000000000000000000001417222763000245400ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/tests/aio/test_async_token_providers.py000066400000000000000000000341421417222763000304700ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import os import pytest from azure.kusto.data._cloud_settings import CloudInfo from azure.kusto.data._decorators import aio_documented_by from azure.kusto.data._token_providers import * from .test_kusto_client import run_aio_tests from ..test_token_providers import KUSTO_URI, TOKEN_VALUE, TEST_AZ_AUTH, TEST_MSI_AUTH, TEST_DEVICE_AUTH, TokenProviderTests, MockProvider @pytest.mark.skipif(not run_aio_tests, reason="requires aio") @aio_documented_by(TokenProviderTests) class TestTokenProvider: @aio_documented_by(TokenProviderTests.test_base_provider) @pytest.mark.asyncio async def test_base_provider(self): # test init with no URI provider = MockProvider(is_async=True) # Test provider with URI, No silent token provider = MockProvider(is_async=True) token = provider._get_token_from_cache_impl() assert provider.init_count == 0 assert token is None token = await provider.get_token_async() assert provider.init_count == 1 assert TokenConstants.MSAL_ACCESS_TOKEN in token token = provider._get_token_from_cache_impl() assert TokenConstants.MSAL_ACCESS_TOKEN in token token = await provider.get_token_async() assert provider.init_count == 1 good_token = {TokenConstants.MSAL_ACCESS_TOKEN: TOKEN_VALUE} bad_token1 = None bad_token2 = {"error": "something bad occurred"} assert provider._valid_token_or_none(good_token) == good_token assert provider._valid_token_or_none(bad_token1) is None assert provider._valid_token_or_none(bad_token2) is None assert provider._valid_token_or_throw(good_token) == good_token exception_occurred = False try: provider._valid_token_or_throw(bad_token1) except KustoClientError: exception_occurred = True finally: assert exception_occurred exception_occurred = False try: provider._valid_token_or_throw(bad_token2) except KustoClientError: exception_occurred = True finally: assert exception_occurred @aio_documented_by(TokenProviderTests.get_token_value) def get_token_value(self, token: dict): assert token is not None assert TokenConstants.MSAL_ERROR not in token value = None if TokenConstants.MSAL_ACCESS_TOKEN in token: return token[TokenConstants.MSAL_ACCESS_TOKEN] elif TokenConstants.AZ_ACCESS_TOKEN in token: return token[TokenConstants.AZ_ACCESS_TOKEN] else: assert False @staticmethod def test_fail_async_call(): provider = BasicTokenProvider(token=TOKEN_VALUE, is_async=True) try: provider.get_token() assert False, "Expected KustoAsyncUsageError to occur" except KustoAsyncUsageError as e: assert str(e) == "Method get_token can't be called from an asynchronous client" try: provider.context() assert False, "Expected KustoAsyncUsageError to occur" except KustoAsyncUsageError as e: assert str(e) == "Method context can't be called from an asynchronous client" @aio_documented_by(TokenProviderTests.test_basic_provider) @pytest.mark.asyncio async def test_basic_provider(self): provider = BasicTokenProvider(token=TOKEN_VALUE, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) == TOKEN_VALUE @aio_documented_by(TokenProviderTests.test_callback_token_provider) @pytest.mark.asyncio async def test_callback_token_provider(self): provider = CallbackTokenProvider(token_callback=lambda: TOKEN_VALUE, async_token_callback=None, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) == TOKEN_VALUE provider = CallbackTokenProvider(token_callback=lambda: 0, async_token_callback=None, is_async=True) # token is not a string exception_occurred = False try: await provider.get_token_async() except KustoClientError: exception_occurred = True finally: assert exception_occurred @pytest.mark.asyncio async def test_callback_token_provider_with_async_method(self): async def callback(): return TOKEN_VALUE provider = CallbackTokenProvider(token_callback=None, async_token_callback=callback, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) == TOKEN_VALUE async def fail_callback(): return 0 provider = CallbackTokenProvider(token_callback=None, async_token_callback=fail_callback, is_async=True) # token is not a string exception_occurred = False try: await provider.get_token_async() except KustoClientError: exception_occurred = True finally: assert exception_occurred @aio_documented_by(TokenProviderTests.test_az_provider) @pytest.mark.asyncio async def test_az_provider(self): if not TEST_AZ_AUTH: print(" *** Skipped Az-Cli Provider Test ***") return print("Note!\nThe test 'test_az_provider' will fail if 'az login' was not called.") provider = AzCliTokenProvider(KUSTO_URI, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) is not None # another run to pass through the cache token = provider._get_token_from_cache_impl() assert self.get_token_value(token) is not None @aio_documented_by(TokenProviderTests.test_msi_provider) @pytest.mark.asyncio async def test_msi_provider(self): if not TEST_MSI_AUTH: print(" *** Skipped MSI Provider Test ***") return user_msi_object_id = os.environ.get("MSI_OBJECT_ID") user_msi_client_id = os.environ.get("MSI_CLIENT_ID") # system MSI provider = MsiTokenProvider(KUSTO_URI, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) is not None if user_msi_object_id is not None: args = {"object_id": user_msi_object_id} provider = MsiTokenProvider(KUSTO_URI, args, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) is not None else: print(" *** Skipped MSI Provider Client Id Test ***") if user_msi_client_id is not None: args = {"client_id": user_msi_client_id} provider = MsiTokenProvider(KUSTO_URI, args, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) is not None else: print(" *** Skipped MSI Provider Object Id Test ***") @aio_documented_by(TokenProviderTests.test_user_pass_provider) @pytest.mark.asyncio async def test_user_pass_provider(self): username = os.environ.get("USER_NAME") password = os.environ.get("USER_PASS") auth = os.environ.get("USER_AUTH_ID", "organizations") if username and password and auth: provider = UserPassTokenProvider(KUSTO_URI, auth, username, password, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert self.get_token_value(token) is not None else: print(" *** Skipped User & Pass Provider Test ***") @aio_documented_by(TokenProviderTests.test_device_auth_provider) @pytest.mark.asyncio async def test_device_auth_provider(self): if not TEST_DEVICE_AUTH: print(" *** Skipped User Device Flow Test ***") return def callback(x): # break here if you debug this test, and get the code from 'x' print(x) provider = DeviceLoginTokenProvider(KUSTO_URI, "organizations", callback, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert self.get_token_value(token) is not None @aio_documented_by(TokenProviderTests.test_app_key_provider) @pytest.mark.asyncio async def test_app_key_provider(self): # default details are for kusto-client-e2e-test-app # to run the test, get the key from Azure portal app_id = os.environ.get("APP_ID", "b699d721-4f6f-4320-bc9a-88d578dfe68f") auth_id = os.environ.get("APP_AUTH_ID", "72f988bf-86f1-41af-91ab-2d7cd011db47") app_key = os.environ.get("APP_KEY") if app_id and app_key and auth_id: provider = ApplicationKeyTokenProvider(KUSTO_URI, auth_id, app_id, app_key, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert self.get_token_value(token) is not None else: print(" *** Skipped App Id & Key Provider Test ***") @aio_documented_by(TokenProviderTests.test_app_cert_provider) @pytest.mark.asyncio async def test_app_cert_provider(self): # default details are for kusto-client-e2e-test-app # to run the test download the certs from Azure Portal cert_app_id = os.environ.get("CERT_APP_ID", "b699d721-4f6f-4320-bc9a-88d578dfe68f") cert_auth = os.environ.get("CERT_AUTH", "72f988bf-86f1-41af-91ab-2d7cd011db47") thumbprint = os.environ.get("CERT_THUMBPRINT") public_cert_path = os.environ.get("PUBLIC_CERT_PATH") pem_key_path = os.environ.get("CERT_PEM_KEY_PATH") if pem_key_path and thumbprint and cert_app_id: with open(pem_key_path, "rb") as file: pem_key = file.read() provider = ApplicationCertificateTokenProvider(KUSTO_URI, cert_app_id, cert_auth, pem_key, thumbprint, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert self.get_token_value(token) is not None if public_cert_path: with open(public_cert_path, "r") as file: public_cert = file.read() provider = ApplicationCertificateTokenProvider(KUSTO_URI, cert_app_id, cert_auth, pem_key, thumbprint, public_cert, is_async=True) token = await provider.get_token_async() assert self.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert self.get_token_value(token) is not None else: print(" *** Skipped App Cert SNI Provider Test ***") else: print(" *** Skipped App Cert Provider Test ***") @aio_documented_by(TokenProviderTests.test_cloud_mfa_off) @pytest.mark.asyncio async def test_cloud_mfa_off(self): FAKE_URI = "https://fake_cluster_for_login_mfa_test.kusto.windows.net" cloud = CloudInfo( login_endpoint="https://login_endpoint", login_mfa_required=False, kusto_client_app_id="1234", kusto_client_redirect_uri="", kusto_service_resource_id="https://fakeurl.kusto.windows.net", first_party_authority_url="", ) CloudSettings._cloud_cache[FAKE_URI] = cloud authority = "auth_test" provider = UserPassTokenProvider(FAKE_URI, authority, "a", "b", is_async=True) await provider._init_once_async(init_only_resources=True) context = await provider.context_async() assert context["authority"] == "https://login_endpoint/auth_test" assert context["client_id"] == "1234" assert provider._scopes == ["https://fakeurl.kusto.windows.net/.default"] @aio_documented_by(TokenProviderTests.test_cloud_mfa_off) @pytest.mark.asyncio async def test_cloud_mfa_on(self): FAKE_URI = "https://fake_cluster_for_login_mfa_test.kusto.windows.net" cloud = CloudInfo( login_endpoint="https://login_endpoint", login_mfa_required=True, kusto_client_app_id="1234", kusto_client_redirect_uri="", kusto_service_resource_id="https://fakeurl.kusto.windows.net", first_party_authority_url="", ) CloudSettings._cloud_cache[FAKE_URI] = cloud authority = "auth_test" provider = UserPassTokenProvider(FAKE_URI, authority, "a", "b", is_async=True) await provider._init_once_async(init_only_resources=True) context = await provider.context_async() assert context["authority"] == "https://login_endpoint/auth_test" assert context["client_id"] == "1234" assert provider._scopes == ["https://fakeurl.kustomfa.windows.net/.default"] def test_async_lock(self): """ This test makes sure that the lock inside of a TokenProvider, is created within the correct event loop. Before this, the Lock was created once per class. This meant that if someone created a new event loop, and created a provider in it, awaiting on the lock would cause an exception because it belongs to a different loop. Now the lock is instantiated for every class instance, avoiding this issue. """ async def start(): async def inner(): await asyncio.sleep(0.1) return "" provider = CallbackTokenProvider(token_callback=None, async_token_callback=inner, is_async=True) await asyncio.gather(provider.get_token_async(), provider.get_token_async(), provider.get_token_async()) loop = asyncio.events.new_event_loop() asyncio.events.set_event_loop(loop) loop.run_until_complete(start()) azure-kusto-python-3.0.1/azure-kusto-data/tests/aio/test_kusto_client.py000066400000000000000000000200161417222763000265540ustar00rootroot00000000000000"""Tests for KustoClient.""" import json import sys from unittest.mock import patch import pytest from azure.kusto.data._cloud_settings import CloudSettings from azure.kusto.data._decorators import aio_documented_by from azure.kusto.data.client import ClientRequestProperties from azure.kusto.data.exceptions import KustoMultiApiError from azure.kusto.data.helpers import dataframe_from_result_table from ..kusto_client_common import KustoClientTestsMixin, mocked_requests_post, proxy_kcsb from ..test_kusto_client import TestKustoClient as KustoClientTestsSync PANDAS = False try: import pandas PANDAS = True except: pass run_aio_tests = False try: from azure.kusto.data.aio.client import KustoClient from aioresponses import aioresponses, CallbackResult run_aio_tests = True except: pass if sys.version_info < (3, 6): run_aio_tests = False @pytest.mark.skipif(not run_aio_tests, reason="requires aio") @aio_documented_by(KustoClientTestsSync) class TestKustoClient(KustoClientTestsMixin): @staticmethod def _mock_callback(url, **kwargs): body = json.dumps(mocked_requests_post(str(url), **kwargs).json()) return CallbackResult(status=200, body=body) def _mock_query(self, aioresponses_mock): url = "{host}/v2/rest/query".format(host=self.HOST) aioresponses_mock.post(url, callback=self._mock_callback) def _mock_mgmt(self, aioresponses_mock): url = "{host}/v1/rest/mgmt".format(host=self.HOST) aioresponses_mock.post(url, callback=self._mock_callback) def _mock_cloud_info(self, aioresponses_mock): url = "{host}/v1/rest/auth/metadata".format(host=self.HOST) aioresponses_mock.get(url, callback=self._mock_callback) @aio_documented_by(KustoClientTestsSync.test_sanity_query) @pytest.mark.asyncio async def test_sanity_query(self): with aioresponses() as aioresponses_mock: self._mock_query(aioresponses_mock) async with KustoClient(self.HOST) as client: response = await client.execute_query("PythonTest", "Deft") first_request = next(iter(aioresponses_mock.requests.values())) self._assert_client_request_id(first_request[0].kwargs) self._assert_sanity_query_response(response) @aio_documented_by(KustoClientTestsSync.test_sanity_control_command) @pytest.mark.asyncio async def test_sanity_control_command(self): with aioresponses() as aioresponses_mock: self._mock_mgmt(aioresponses_mock) async with KustoClient(self.HOST) as client: response = await client.execute_mgmt("NetDefaultDB", ".show version") self._assert_sanity_control_command_response(response) @pytest.mark.skipif(not PANDAS, reason="requires pandas") @aio_documented_by(KustoClientTestsSync.test_sanity_data_frame) @pytest.mark.asyncio async def test_sanity_data_frame(self): with aioresponses() as aioresponses_mock: self._mock_query(aioresponses_mock) async with KustoClient(self.HOST) as client: response = await client.execute_query("PythonTest", "Deft") data_frame = dataframe_from_result_table(response.primary_results[0]) self._assert_sanity_data_frame_response(data_frame) @aio_documented_by(KustoClientTestsSync.test_partial_results) @pytest.mark.asyncio async def test_partial_results(self): async with KustoClient(self.HOST) as client: query = """set truncationmaxrecords = 5; range x from 1 to 10 step 1""" properties = ClientRequestProperties() properties.set_option(ClientRequestProperties.results_defer_partial_query_failures_option_name, False) with aioresponses() as aioresponses_mock: self._mock_query(aioresponses_mock) with pytest.raises(KustoMultiApiError) as e: await client.execute_query("PythonTest", query, properties) errors = e.value.get_api_errors() assert len(errors) == 1 assert errors[0].code == "LimitsExceeded" properties.set_option(ClientRequestProperties.results_defer_partial_query_failures_option_name, True) self._mock_query(aioresponses_mock) response = await client.execute_query("PythonTest", query, properties) self._assert_partial_results_response(response) @aio_documented_by(KustoClientTestsSync.test_admin_then_query) @pytest.mark.asyncio async def test_admin_then_query(self): with aioresponses() as aioresponses_mock: self._mock_mgmt(aioresponses_mock) async with KustoClient(self.HOST) as client: query = ".show tables | project DatabaseName, TableName" response = await client.execute_mgmt("PythonTest", query) self._assert_admin_then_query_response(response) @aio_documented_by(KustoClientTestsSync.test_dynamic) @pytest.mark.asyncio async def test_dynamic(self): with aioresponses() as aioresponses_mock: self._mock_query(aioresponses_mock) async with KustoClient(self.HOST) as client: query = """print dynamic(123), dynamic("123"), dynamic("test bad json"),""" """ dynamic(null), dynamic('{"rowId":2,"arr":[0,2]}'), dynamic({"rowId":2,"arr":[0,2]})""" response = await client.execute_query("PythonTest", query) row = response.primary_results[0].rows[0] self._assert_dynamic_response(row) @aio_documented_by(KustoClientTestsSync.test_empty_result) @pytest.mark.asyncio async def test_empty_result(self): with aioresponses() as aioresponses_mock: self._mock_query(aioresponses_mock) async with KustoClient(self.HOST) as client: query = """print 'a' | take 0""" response = await client.execute_query("PythonTest", query) assert response.primary_results[0] @aio_documented_by(KustoClientTestsSync.test_null_values_in_data) @pytest.mark.asyncio async def test_null_values_in_data(self): with aioresponses() as aioresponses_mock: self._mock_query(aioresponses_mock) async with KustoClient(self.HOST) as client: query = "PrimaryResultName" response = await client.execute_query("PythonTest", query) assert response is not None @aio_documented_by(KustoClientTestsSync.test_sanity_query) @pytest.mark.asyncio async def test_request_id(self): with aioresponses() as aioresponses_mock: properties = ClientRequestProperties() request_id = "test_request_id" properties.client_request_id = request_id self._mock_query(aioresponses_mock) async with KustoClient(self.HOST) as client: response = await client.execute_query("PythonTest", "Deft", properties=properties) first_request = next(iter(aioresponses_mock.requests.values())) self._assert_client_request_id(first_request[0].kwargs, value=request_id) self._assert_sanity_query_response(response) @aio_documented_by(KustoClientTestsSync.test_proxy_token_providers) @pytest.mark.asyncio async def test_proxy_token_providers(self, proxy_kcsb): """Test query V2.""" proxy = "https://my_proxy.sample" kcsb, auth_supports_proxy = proxy_kcsb async with KustoClient(kcsb) as client: client.set_proxy(proxy) assert client._proxy_url == proxy expected_dict = {"http": proxy, "https": proxy} if not auth_supports_proxy: return assert client._aad_helper.token_provider._proxy_dict == expected_dict CloudSettings._cloud_cache.clear() with patch("requests.get", side_effect=mocked_requests_post) as mock_get: client._aad_helper.token_provider._init_resources() mock_get.assert_called_with("https://somecluster.kusto.windows.net/v1/rest/auth/metadata", proxies=expected_dict) azure-kusto-python-3.0.1/azure-kusto-data/tests/input/000077500000000000000000000000001417222763000230305ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-data/tests/input/adminthenquery.json000066400000000000000000000060311417222763000267600ustar00rootroot00000000000000{ "Tables": [{ "TableName": "Table_0", "Columns": [{ "ColumnName": "DatabaseName", "DataType": "String" }, { "ColumnName": "TableName", "DataType": "String" }], "Rows": [["Kuskus", "KustoLogs"], ["Kuskus", "LiorTmp"]] }, { "TableName": "Table_1", "Columns": [{ "ColumnName": "Value", "DataType": "String" }], "Rows": [["{\"Visualization\": null,\"Title\": null,\"XColumn\": null,\"Series\": null,\"YColumns\": null,\"XTitle\": null,\"YTitle\": null,\"XAxis\": null,\"YAxis\": null,\"Legend\": null,\"YSplit\": null,\"Accumulate\": false,\"IsQuerySorted\": false,\"Kind\": null}"]] }, { "TableName": "Table_2", "Columns": [{ "ColumnName": "Timestamp", "DataType": "DateTime" }, { "ColumnName": "Severity", "DataType": "Int32" }, { "ColumnName": "SeverityName", "DataType": "String" }, { "ColumnName": "StatusCode", "DataType": "Int32" }, { "ColumnName": "StatusDescription", "DataType": "String" }, { "ColumnName": "Count", "DataType": "Int32" }, { "ColumnName": "RequestId", "DataType": "Guid" }, { "ColumnName": "ActivityId", "DataType": "Guid" }, { "ColumnName": "SubActivityId", "DataType": "Guid" }, { "ColumnName": "ClientActivityId", "DataType": "String" }], "Rows": [["2018-08-12T09:13:19.5200972Z", 4, "Info", 0, "Querycompletedsuccessfully", 1, "b6651693-9325-41c8-a5bf-dcc21202cdf2", "b6651693-9325-41c8-a5bf-dcc21202cdf2", "1cfa59c2-7f29-4e58-aef8-590d4609818f", "KPC.execute;721add30-f61a-44d0-ad89-812d06736260"], ["2018-08-12T09:13:19.5200972Z", 6, "Stats", 0, { "ExecutionTime": 0.0, "resource_usage": { "cache": { "memory": { "hits": 0, "misses": 0, "total": 0 }, "disk": { "hits": 0, "misses": 0, "total": 0 } }, "cpu": { "user": "00:00:00", "kernel": "00:00:00", "total cpu": "00:00:00" }, "memory": { "peak_per_node": 0 } }, "input_dataset_statistics": { "extents": { "total": 0, "scanned": 0 }, "rows": { "total": 0, "scanned": 0 } }, "dataset_statistics": [{ "table_row_count": 2, "table_size": 46 }] }, 1, "b6651693-9325-41c8-a5bf-dcc21202cdf2", "b6651693-9325-41c8-a5bf-dcc21202cdf2", "1cfa59c2-7f29-4e58-aef8-590d4609818f", "KPC.execute;721add30-f61a-44d0-ad89-812d06736260"]] }, { "TableName": "Table_3", "Columns": [{ "ColumnName": "Ordinal", "DataType": "Int64" }, { "ColumnName": "Kind", "DataType": "String" }, { "ColumnName": "Name", "DataType": "String" }, { "ColumnName": "Id", "DataType": "String" }, { "ColumnName": "PrettyName", "DataType": "String" }], "Rows": [[0, "QueryResult", "PrimaryResult", "d6331ef2-d9f7-4d8c-8268-99f574babc82", ""], [1, "QueryProperties", "@ExtendedProperties", "876ccb1a-818e-431f-9147-6b72547cca3d", ""], [2, "QueryStatus", "QueryStatus", "00000000-0000-0000-0000-000000000000", ""]] }] } azure-kusto-python-3.0.1/azure-kusto-data/tests/input/alternative_order.json000066400000000000000000000222331417222763000274360ustar00rootroot00000000000000[ { "FrameType": "DataSetHeader", "Version": "v2.0", "IsProgressive": false }, { "FrameType": "DataTable", "TableId": 0, "TableName": "@ExtendedProperties", "Columns": [ { "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" } ], "TableKind": "QueryProperties", "Rows": [ [ 1, "Visualization", "{\"Visualization\":null,\"Title\":null,\"XColumn\":null,\"Series\":null,\"YColumns\":null,\"XTitle\":null,\"YTitle\":null,\"XAxis\":null,\"YAxis\":null,\"Legend\":null,\"YSplit\":null,\"Accumulate\":false,\"IsQuerySorted\":false,\"Kind\":null}" ] ] }, { "FrameType": "DataTable", "Columns": [ { "ColumnName": "rownumber", "ColumnType": "int" }, { "ColumnName": "rowguid", "ColumnType": "string" }, { "ColumnName": "xdouble", "ColumnType": "real" }, { "ColumnName": "xfloat", "ColumnType": "real" }, { "ColumnName": "xbool", "ColumnType": "bool" }, { "ColumnName": "xint16", "ColumnType": "int" }, { "ColumnName": "xint32", "ColumnType": "int" }, { "ColumnName": "xint64", "ColumnType": "long" }, { "ColumnName": "xuint8", "ColumnType": "long" }, { "ColumnName": "xuint16", "ColumnType": "long" }, { "ColumnName": "xuint32", "ColumnType": "long" }, { "ColumnName": "xuint64", "ColumnType": "long" }, { "ColumnName": "xdate", "ColumnType": "datetime" }, { "ColumnName": "xsmalltext", "ColumnType": "string" }, { "ColumnName": "xtext", "ColumnType": "string" }, { "ColumnName": "xnumberAsText", "ColumnType": "string" }, { "ColumnName": "xtime", "ColumnType": "timespan" }, { "ColumnName": "xtextWithNulls", "ColumnType": "string" }, { "ColumnName": "xdynamicWithNulls", "ColumnType": "dynamic" } ], "TableId": 1, "TableKind": "PrimaryResult", "TableName": "Deft", "Rows": [ [ null, "", null, null, null, null, null, null, null, null, null, null, null, "", "", "", null, "", "" ], [ 0, "00000000-0000-0000-0001-020304050607", 0.0, 0.0, false, 0, 0, 0, 0, 0, 0, 0, "2014-01-01T01:01:01.0000000Z", "Zero", "Zero", "0", "00:00:00", "", "" ], [ 1, "00000001-0000-0000-0001-020304050607", 1.0001, 1.01, true, 1, 1, 1, 1, 1, 1, 1, "2015-01-01T01:01:01.0000001Z", "One", "One", "1", "1.00:00:01.0010001", "", { "rowId": 1, "arr": [ 0, 1 ] } ], [ 2, "00000002-0000-0000-0001-020304050607", 2.0002, 2.02, false, 2, 2, 2, 2, 2, 2, 2, "2016-01-01T01:01:01.0000002Z", "Two", "Two", "2", "-2.00:00:02.0020002", "", { "rowId": 2, "arr": [ 0, 2 ] } ], [ 3, "00000003-0000-0000-0001-020304050607", 3.0003, 3.03, true, 3, 3, 3, 3, 3, 3, 3, "2017-01-01T01:01:01.0000003Z", "Three", "Three", "3", "3.00:00:03.0030003", "", { "rowId": 3, "arr": [ 0, 3 ] } ], [ 4, "00000004-0000-0000-0001-020304050607", 4.0004, 4.04, false, 4, 4, 4, 4, 4, 4, 4, "2018-01-01T01:01:01.0000004Z", "Four", "Four", "4", "-4.00:00:04.0040004", "", { "rowId": 4, "arr": [ 0, 4 ] } ], [ 5, "00000005-0000-0000-0001-020304050607", 5.0005, 5.05, true, 5, 5, 5, 5, 5, 5, 5, "2019-01-01T01:01:01.0000005Z", "Five", "Five", "5", "5.00:00:05.0050005", "", { "rowId": 5, "arr": [ 0, 5 ] } ], [ 6, "00000006-0000-0000-0001-020304050607", 6.0006, 6.06, false, 6, 6, 6, 6, 6, 6, 6, "2020-01-01T01:01:01.0000006Z", "Six", "Six", "6", "-6.00:00:06.0060006", "", { "rowId": 6, "arr": [ 0, 6 ] } ], [ 7, "00000007-0000-0000-0001-020304050607", 7.0007, 7.07, true, 7, 7, 7, 7, 7, 7, 7, "2021-01-01T01:01:01.0000007Z", "Seven", "Seven", "7", "7.00:00:07.0070007", "", { "rowId": 7, "arr": [ 0, 7 ] } ], [ 8, "00000008-0000-0000-0001-020304050607", 8.0008, 8.08, false, 8, 8, 8, 8, 8, 8, 8, "2022-01-01T01:01:01.0000008Z", "Eight", "Eight", "8", "-8.00:00:08.0080008", "", { "rowId": 8, "arr": [ 0, 8 ] } ], [ 9, "00000009-0000-0000-0001-020304050607", 9.0009, 9.09, true, 9, 9, 9, 9, 9, 9, 9, "2023-01-01T01:01:01.0000009Z", "Nine", "Nine", "9", "9.00:00:09.0090009", "", { "rowId": 9, "arr": [ 0, 9 ] } ] ] }, { "FrameType": "DataTable", "Columns": [ { "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" } ], "TableId": 2, "TableName": "QueryCompletionInformation", "TableKind": "QueryCompletionInformation", "Rows": [ [ "2018-04-30T12:25:11.0778067Z", "unspecified;cc5ee2b9-9b77-4509-9a61-84ec8f0159c2", "eeac049e-8a7d-4188-b797-6b5f2c9f9526", "c6fb9714-5183-4092-8a02-825bd7aa1aee", "df0aaeb0-4c8a-4d77-bc77-f714a4484a6b", 4, "Info", 0, "S_OK (0)", 4, "QueryInfo", "{\"Count\":1,\"Text\":\"Query completed successfully\"}" ], [ "2018-04-30T12:25:11.0778067Z", "unspecified;cc5ee2b9-9b77-4509-9a61-84ec8f0159c2", "eeac049e-8a7d-4188-b797-6b5f2c9f9526", "c6fb9714-5183-4092-8a02-825bd7aa1aee", "df0aaeb0-4c8a-4d77-bc77-f714a4484a6b", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\"ExecutionTime\":0.0156154,\"resource_usage\":{\"cache\":{\"memory\":{\"hits\":40,\"misses\":0,\"total\":40},\"disk\":{\"hits\":0,\"misses\":0,\"total\":0}},\"cpu\":{\"user\":\"00:00:00\",\"kernel\":\"00:00:00\",\"total cpu\":\"00:00:00\"},\"memory\":{\"peak_per_node\":0}},\"dataset_statistics\":[{\"table_row_count\":11,\"table_size\":2444}]}" ] ] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false } ]azure-kusto-python-3.0.1/azure-kusto-data/tests/input/dataframe.json000066400000000000000000000103451417222763000256520ustar00rootroot00000000000000[{ "FrameType": "DataSetHeader", "IsProgressive": false, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableName": "@ExtendedProperties", "TableKind": "QueryProperties", "Columns": [{ "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" }], "Rows": [[1, "Visualization", "{\"Visualization\":null,\"Title\":null,\"XColumn\":null,\"Series\":null,\"YColumns\":null,\"XTitle\":null,\"YTitle\":null,\"XAxis\":null,\"YAxis\":null,\"Legend\":null,\"YSplit\":null,\"Accumulate\":false,\"IsQuerySorted\":false,\"Kind\":null}"]] }, { "FrameType": "DataTable", "TableId": 1, "TableName": "temp", "TableKind": "PrimaryResult", "Columns": [{ "ColumnName": "RecordName", "ColumnType": "string" }, { "ColumnName": "RecordTime", "ColumnType": "datetime" }, { "ColumnName": "RecordOffset", "ColumnType": "timespan" }, { "ColumnName": "RecordBool", "ColumnType": "bool" }, { "ColumnName": "RecordInt", "ColumnType": "int" }, { "ColumnName": "RecordReal", "ColumnType": "real" }], "Rows": [["now", "2021-12-22T11:43:00Z", 0, true, 5678, 3.14159], ["earliest datetime", "0000-01-01T00:00:00Z", 0, true, 5678, NaN], ["latest datetime", "9999-12-31T23:59:59Z", 0, true, 5678, Infinity], ["earliest pandas datetime", "1677-09-21T00:12:44Z", 0, true, 5678, -Infinity], ["latest pandas datetime", "2262-04-11T23:47:16Z", 0, true, 5678, 3.14159], ["timedelta ticks", "2021-12-22T11:43:00Z", 600000000, true, 5678, 3.14159], ["timedelta string", "2021-12-22T11:43:00Z", "1.01:01:01.0", true, 5678, 3.14159], [null, "", 0, false, 0, 0]] }, { "FrameType": "DataTable", "TableId": 2, "TableName": "QueryCompletionInformation", "TableKind": "QueryCompletionInformation", "Columns": [{ "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" }], "Rows": [["2018-05-01T09:32:38.916566Z", "unspecified;e8e72755-786b-4bdc-835d-ea49d63d09fd", "5935a050-e466-48a0-991d-0ec26bd61c7e", "8182b177-7a80-4158-aca8-ff4fd8e7d3f8", "6f3c1072-2739-461c-8aa7-3cfc8ff528a8", 4, "Info", 0, "S_OK (0)", 4, "QueryInfo", "{\"Count\":1,\"Text\":\"Querycompletedsuccessfully\"}"], ["2018-05-01T09:32:38.916566Z", "unspecified;e8e72755-786b-4bdc-835d-ea49d63d09fd", "5935a050-e466-48a0-991d-0ec26bd61c7e", "8182b177-7a80-4158-aca8-ff4fd8e7d3f8", "6f3c1072-2739-461c-8aa7-3cfc8ff528a8", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\"ExecutionTime\":0.0156222,\"resource_usage\":{\"cache\":{\"memory\":{\"hits\":13,\"misses\":0,\"total\":13},\"disk\":{\"hits\":0,\"misses\":0,\"total\":0}},\"cpu\":{\"user\":\"00: 00: 00\",\"kernel\":\"00: 00: 00\",\"totalcpu\":\"00: 00: 00\"},\"memory\":{\"peak_per_node\":16777312}},\"dataset_statistics\":[{\"table_row_count\":3,\"table_size\":191}]}"]] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false }]azure-kusto-python-3.0.1/azure-kusto-data/tests/input/deft.json000066400000000000000000000271611417222763000246540ustar00rootroot00000000000000[ { "FrameType": "DataSetHeader", "IsProgressive": false, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableName": "@ExtendedProperties", "TableKind": "QueryProperties", "Columns": [ { "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" } ], "Rows": [ [ 1, "Visualization", "{\"Visualization\":null,\"Title\":null,\"XColumn\":null,\"Series\":null,\"YColumns\":null,\"XTitle\":null,\"YTitle\":null,\"XAxis\":null,\"YAxis\":null,\"Legend\":null,\"YSplit\":null,\"Accumulate\":false,\"IsQuerySorted\":false,\"Kind\":null}" ] ] }, { "FrameType": "DataTable", "TableId": 1, "TableName": "Deft", "TableKind": "PrimaryResult", "Columns": [ { "ColumnName": "rownumber", "ColumnType": "int" }, { "ColumnName": "rowguid", "ColumnType": "string" }, { "ColumnName": "xdouble", "ColumnType": "real" }, { "ColumnName": "xfloat", "ColumnType": "real" }, { "ColumnName": "xbool", "ColumnType": "bool" }, { "ColumnName": "xint16", "ColumnType": "int" }, { "ColumnName": "xint32", "ColumnType": "int" }, { "ColumnName": "xint64", "ColumnType": "long" }, { "ColumnName": "xuint8", "ColumnType": "long" }, { "ColumnName": "xuint16", "ColumnType": "long" }, { "ColumnName": "xuint32", "ColumnType": "long" }, { "ColumnName": "xuint64", "ColumnType": "long" }, { "ColumnName": "xdate", "ColumnType": "datetime" }, { "ColumnName": "xsmalltext", "ColumnType": "string" }, { "ColumnName": "xtext", "ColumnType": "string" }, { "ColumnName": "xnumberAsText", "ColumnType": "string" }, { "ColumnName": "xtime", "ColumnType": "timespan" }, { "ColumnName": "xtextWithNulls", "ColumnType": "string" }, { "ColumnName": "xdynamicWithNulls", "ColumnType": "dynamic" } ], "Rows": [ [ null, "", null, null, null, null, null, null, null, null, null, null, null, "", "", "", null, "", "" ], [ 0, "00000000-0000-0000-0001-020304050607", 0.0, 0.0, false, 0, 0, 0, 0, 0, 0, 0, "2014-01-01T01:01:01.0000000Z", "Zero", "Zero", "0", "00:00:00", "", "" ], [ 1, "00000001-0000-0000-0001-020304050607", 1.0001, 1.01, true, 1, 1, 1, 1, 1, 1, 1, "2015-01-01T01:01:01.0000001Z", "One", "One", "1", "1.00:00:01.0010001", "", {"rowId":1,"arr":[0,1]} ], [ 2, "00000002-0000-0000-0001-020304050607", 2.0002, 2.02, false, 2, 2, 2, 2, 2, 2, 2, "2016-01-01T01:01:01.0000002Z", "Two", "Two", "2", "-2.00:00:02.0020002", "", {"rowId":2,"arr":[0,2]} ], [ 3, "00000003-0000-0000-0001-020304050607", 3.0003, 3.03, true, 3, 3, 3, 3, 3, 3, 3, "2017-01-01T01:01:01.0000003Z", "Three", "Three", "3", "3.00:00:03.0030003", "", {"rowId":3,"arr":[0,3]} ], [ 4, "00000004-0000-0000-0001-020304050607", 4.0004, 4.04, false, 4, 4, 4, 4, 4, 4, 4, "2018-01-01T01:01:01.0000004Z", "Four", "Four", "4", "-4.00:00:04.0040004", "", {"rowId":4,"arr":[0,4]} ], [ 5, "00000005-0000-0000-0001-020304050607", 5.0005, 5.05, true, 5, 5, 5, 5, 5, 5, 5, "2019-01-01T01:01:01.0000005Z", "Five", "Five", "5", "5.00:00:05.0050005", "", {"rowId":5,"arr":[0,5]} ], [ 6, "00000006-0000-0000-0001-020304050607", 6.0006, 6.06, false, 6, 6, 6, 6, 6, 6, 6, "2020-01-01T01:01:01.0000006Z", "Six", "Six", "6", "-6.00:00:06.0060006", "", {"rowId":6,"arr":[0,6]} ], [ 7, "00000007-0000-0000-0001-020304050607", 7.0007, 7.07, true, 7, 7, 7, 7, 7, 7, 7, "2021-01-01T01:01:01.0000007Z", "Seven", "Seven", "7", "7.00:00:07.0070007", "", {"rowId":7,"arr":[0,7]} ], [ 8, "00000008-0000-0000-0001-020304050607", 8.0008, 8.08, false, 8, 8, 8, 8, 8, 8, 8, "2022-01-01T01:01:01.0000008Z", "Eight", "Eight", "8", "-8.00:00:08.0080008", "", {"rowId":8,"arr":[0,8]} ], [ 9, "00000009-0000-0000-0001-020304050607", 9.0009, 9.09, true, 9, 9, 9, 9, 9, 9, 9, "2023-01-01T01:01:01.0000009Z", "Nine", "Nine", "9", "9.00:00:09.0090009", "", {"rowId":9,"arr":[0,9]} ] ] }, { "FrameType": "DataTable", "TableId": 2, "TableName": "QueryCompletionInformation", "TableKind": "QueryCompletionInformation", "Columns": [ { "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" } ], "Rows": [ [ "2018-04-30T12:25:11.0778067Z", "unspecified;cc5ee2b9-9b77-4509-9a61-84ec8f0159c2", "eeac049e-8a7d-4188-b797-6b5f2c9f9526", "c6fb9714-5183-4092-8a02-825bd7aa1aee", "df0aaeb0-4c8a-4d77-bc77-f714a4484a6b", 4, "Info", 0, "S_OK (0)", 4, "QueryInfo", "{\"Count\":1,\"Text\":\"Query completed successfully\"}" ], [ "2018-04-30T12:25:11.0778067Z", "unspecified;cc5ee2b9-9b77-4509-9a61-84ec8f0159c2", "eeac049e-8a7d-4188-b797-6b5f2c9f9526", "c6fb9714-5183-4092-8a02-825bd7aa1aee", "df0aaeb0-4c8a-4d77-bc77-f714a4484a6b", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\"ExecutionTime\":0.0156154,\"resource_usage\":{\"cache\":{\"memory\":{\"hits\":40,\"misses\":0,\"total\":40},\"disk\":{\"hits\":0,\"misses\":0,\"total\":0}},\"cpu\":{\"user\":\"00:00:00\",\"kernel\":\"00:00:00\",\"total cpu\":\"00:00:00\"},\"memory\":{\"peak_per_node\":0}},\"dataset_statistics\":[{\"table_row_count\":11,\"table_size\":2444}]}" ] ] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false } ]azure-kusto-python-3.0.1/azure-kusto-data/tests/input/deft_with_progressive_result.json000066400000000000000000000223611417222763000317320ustar00rootroot00000000000000[ { "FrameType": "DataSetHeader", "IsProgressive": false, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableName": "@ExtendedProperties", "TableKind": "QueryProperties", "Columns": [ { "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" } ], "Rows": [ [ 1, "Visualization", "{\"Visualization\":null,\"Title\":null,\"XColumn\":null,\"Series\":null,\"YColumns\":null,\"XTitle\":null,\"YTitle\":null,\"XAxis\":null,\"YAxis\":null,\"Legend\":null,\"YSplit\":null,\"Accumulate\":false,\"IsQuerySorted\":false,\"Kind\":null}" ] ] }, { "FrameType": "DataTable", "TableId": 1, "TableName": "Deft", "TableKind": "PrimaryResult", "Columns": [ { "ColumnName": "rownumber", "ColumnType": "int" }, { "ColumnName": "rowguid", "ColumnType": "string" }, { "ColumnName": "xdouble", "ColumnType": "real" }, { "ColumnName": "xfloat", "ColumnType": "real" }, { "ColumnName": "xbool", "ColumnType": "bool" }, { "ColumnName": "xint16", "ColumnType": "int" }, { "ColumnName": "xint32", "ColumnType": "int" }, { "ColumnName": "xint64", "ColumnType": "long" }, { "ColumnName": "xuint8", "ColumnType": "long" }, { "ColumnName": "xuint16", "ColumnType": "long" }, { "ColumnName": "xuint32", "ColumnType": "long" }, { "ColumnName": "xuint64", "ColumnType": "long" }, { "ColumnName": "xdate", "ColumnType": "datetime" }, { "ColumnName": "xsmalltext", "ColumnType": "string" }, { "ColumnName": "xtext", "ColumnType": "string" }, { "ColumnName": "xnumberAsText", "ColumnType": "string" }, { "ColumnName": "xtime", "ColumnType": "timespan" }, { "ColumnName": "xtextWithNulls", "ColumnType": "string" }, { "ColumnName": "xdynamicWithNulls", "ColumnType": "dynamic" } ], "Rows": [ [ null, "", null, null, null, null, null, null, null, null, null, null, null, "", "", "", null, "", "" ], [ 0, "00000000-0000-0000-0001-020304050607", 0.0, 0.0, false, 0, 0, 0, 0, 0, 0, 0, "2014-01-01T01:01:01.0000000Z", "Zero", "Zero", "0", "00:00:00", "", "" ], [ 1, "00000001-0000-0000-0001-020304050607", 1.0001, 1.01, true, 1, 1, 1, 1, 1, 1, 1, "2015-01-01T01:01:01.0000001Z", "One", "One", "1", "1.00:00:01.0010001", "", { "rowId": 1, "arr": [ 0, 1 ] } ], [ 2, "00000002-0000-0000-0001-020304050607", 2.0002, 2.02, false, 2, 2, 2, 2, 2, 2, 2, "2016-01-01T01:01:01.0000002Z", "Two", "Two", "2", "-2.00:00:02.0020002", "", { "rowId": 2, "arr": [ 0, 2 ] } ], [ 3, "00000003-0000-0000-0001-020304050607", 3.0003, 3.03, true, 3, 3, 3, 3, 3, 3, 3, "2017-01-01T01:01:01.0000003Z", "Three", "Three", "3", "3.00:00:03.0030003", "", { "rowId": 3, "arr": [ 0, 3 ] } ], [ 4, "00000004-0000-0000-0001-020304050607", 4.0004, 4.04, false, 4, 4, 4, 4, 4, 4, 4, "2018-01-01T01:01:01.0000004Z", "Four", "Four", "4", "-4.00:00:04.0040004", "", { "rowId": 4, "arr": [ 0, 4 ] } ], [ 5, "00000005-0000-0000-0001-020304050607", 5.0005, 5.05, true, 5, 5, 5, 5, 5, 5, 5, "2019-01-01T01:01:01.0000005Z", "Five", "Five", "5", "5.00:00:05.0050005", "", { "rowId": 5, "arr": [ 0, 5 ] } ], [ 6, "00000006-0000-0000-0001-020304050607", 6.0006, 6.06, false, 6, 6, 6, 6, 6, 6, 6, "2020-01-01T01:01:01.0000006Z", "Six", "Six", "6", "-6.00:00:06.0060006", "", { "rowId": 6, "arr": [ 0, 6 ] } ], [ 7, "00000007-0000-0000-0001-020304050607", 7.0007, 7.07, true, 7, 7, 7, 7, 7, 7, 7, "2021-01-01T01:01:01.0000007Z", "Seven", "Seven", "7", "7.00:00:07.0070007", "", { "rowId": 7, "arr": [ 0, 7 ] } ], [ 8, "00000008-0000-0000-0001-020304050607", 8.0008, 8.08, false, 8, 8, 8, 8, 8, 8, 8, "2022-01-01T01:01:01.0000008Z", "Eight", "Eight", "8", "-8.00:00:08.0080008", "", { "rowId": 8, "arr": [ 0, 8 ] } ], [ 9, "00000009-0000-0000-0001-020304050607", 9.0009, 9.09, true, 9, 9, 9, 9, 9, 9, 9, "2023-01-01T01:01:01.0000009Z", "Nine", "Nine", "9", "9.00:00:09.0090009", "", { "rowId": 9, "arr": [ 0, 9 ] } ] ] }, { "FrameType": "TableProgress", "TableId": 1, "TableProgress": 0.0 }, { "FrameType": "DataTable", "TableId": 2, "TableName": "QueryCompletionInformation", "TableKind": "QueryCompletionInformation", "Columns": [ { "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" } ], "Rows": [ [ "2018-04-30T12:25:11.0778067Z", "unspecified;cc5ee2b9-9b77-4509-9a61-84ec8f0159c2", "eeac049e-8a7d-4188-b797-6b5f2c9f9526", "c6fb9714-5183-4092-8a02-825bd7aa1aee", "df0aaeb0-4c8a-4d77-bc77-f714a4484a6b", 4, "Info", 0, "S_OK (0)", 4, "QueryInfo", "{\"Count\":1,\"Text\":\"Query completed successfully\"}" ], [ "2018-04-30T12:25:11.0778067Z", "unspecified;cc5ee2b9-9b77-4509-9a61-84ec8f0159c2", "eeac049e-8a7d-4188-b797-6b5f2c9f9526", "c6fb9714-5183-4092-8a02-825bd7aa1aee", "df0aaeb0-4c8a-4d77-bc77-f714a4484a6b", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\"ExecutionTime\":0.0156154,\"resource_usage\":{\"cache\":{\"memory\":{\"hits\":40,\"misses\":0,\"total\":40},\"disk\":{\"hits\":0,\"misses\":0,\"total\":0}},\"cpu\":{\"user\":\"00:00:00\",\"kernel\":\"00:00:00\",\"total cpu\":\"00:00:00\"},\"memory\":{\"peak_per_node\":0}},\"dataset_statistics\":[{\"table_row_count\":11,\"table_size\":2444}]}" ] ] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false } ]azure-kusto-python-3.0.1/azure-kusto-data/tests/input/dynamic.json000066400000000000000000000066201417222763000253530ustar00rootroot00000000000000[{ "FrameType": "DataSetHeader", "IsProgressive": false, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableKind": "QueryProperties", "TableName": "@ExtendedProperties", "Columns": [{ "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" }], "Rows": [[1, "Visualization", "{\"Visualization\":null,\"Title\":null,\"XColumn\":null,\"Series\":null,\"YColumns\":null,\"AnomalyColumns\":null,\"XTitle\":null,\"YTitle\":null,\"XAxis\":null,\"YAxis\":null,\"Legend\":null,\"YSplit\":null,\"Accumulate\":false,\"IsQuerySorted\":false,\"Kind\":null,\"Ymin\":\"NaN\",\"Ymax\":\"NaN\"}"]] }, { "FrameType": "DataTable", "TableId": 1, "TableKind": "PrimaryResult", "TableName": "PrimaryResult", "Columns": [{ "ColumnName": "print_0", "ColumnType": "dynamic" }, { "ColumnName": "print_1", "ColumnType": "dynamic" }, { "ColumnName": "print_2", "ColumnType": "dynamic" }, { "ColumnName": "print_3", "ColumnType": "dynamic" }, { "ColumnName": "print_4", "ColumnType": "dynamic" }, { "ColumnName": "print_5", "ColumnType": "dynamic" }], "Rows": [[123, "123", "test bad json", null, "{\"rowId\":2,\"arr\":[0,2]}", { "rowId": 2, "arr": [0, 2] }]] }, { "FrameType": "DataTable", "TableId": 2, "TableKind": "QueryCompletionInformation", "TableName": "QueryCompletionInformation", "Columns": [{ "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" }], "Rows": [["2019-02-10T12:07:01.0562684Z", "KPC.execute;f0131f65-d1ed-4c9d-9110-ddf9879f7ff6", "4400a720-f11d-43b6-9097-63462d35bcd4", "bf1b857c-e857-44f6-8d8c-156081bfb92c", "57e0f58d-398c-490b-b900-9f308c531af7", 4, "Info", 0, "S_OK (0)", 4, "QueryInfo", "{\"Count\":1,\"Text\":\"Query completed successfully\"}"], ["2019-02-10T12:07:01.0562684Z", "KPC.execute;f0131f65-d1ed-4c9d-9110-ddf9879f7ff6", "4400a720-f11d-43b6-9097-63462d35bcd4", "bf1b857c-e857-44f6-8d8c-156081bfb92c", "57e0f58d-398c-490b-b900-9f308c531af7", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\"ExecutionTime\":0.1875076,\"resource_usage\":{\"cache\":{\"memory\":{\"hits\":0,\"misses\":0,\"total\":0},\"disk\":{\"hits\":0,\"misses\":0,\"total\":0},\"shards\":{\"hitbytes\":0,\"missbytes\":0,\"bypassbytes\":0}},\"cpu\":{\"user\":\"00:00:00\",\"kernel\":\"00:00:00\",\"total cpu\":\"00:00:00\"},\"memory\":{\"peak_per_node\":0}},\"input_dataset_statistics\":{\"extents\":{\"total\":0,\"scanned\":0},\"rows\":{\"total\":0,\"scanned\":0},\"rowstores\":{\"scanned_rows\":0,\"scanned_values_size\":0}},\"dataset_statistics\":[{\"table_row_count\":1,\"table_size\":141}]}"]] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false }]azure-kusto-python-3.0.1/azure-kusto-data/tests/input/null_values.json000066400000000000000000000064601417222763000262620ustar00rootroot00000000000000[{ "FrameType": "DataSetHeader", "IsProgressive": false, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableKind": "QueryProperties", "TableName": "@ExtendedProperties", "Columns": [{ "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" }], "Rows": [[null, null, null]] }, { "FrameType": "DataTable", "TableId": 1, "TableKind": "PrimaryResult", "TableName": "PrimaryResultName", "Columns": [{ "ColumnName": "String", "ColumnType": "string" }, { "ColumnName": "Int", "ColumnType": "int" }, { "ColumnName": "Bool", "ColumnType": "bool" }, { "ColumnName": "Datetime", "ColumnType": "datetime" }, { "ColumnName": "Dynamic", "ColumnType": "dynamic" }, { "ColumnName": "Guid", "ColumnType": "guid" }, { "ColumnName": "Long", "ColumnType": "long" }, { "ColumnName": "Real", "ColumnType": "real" }, { "ColumnName": "Timespan", "ColumnType": "timespan" }, { "ColumnName": "Decimal", "ColumnType": "decimal" }], "Rows": [[null,null,null,null,null,null,null,null,null,null]] }, { "FrameType": "DataTable", "TableId": 2, "TableKind": "QueryCompletionInformation", "TableName": "QueryCompletionInformation", "Columns": [{ "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" }], "Rows": [["2019-02-12T10:23:02.0413963Z", "KPC.execute;57050c90-8a7d-4b29-b8d0-a40688a8185c", "dfbaa865-e29d-46e0-af17-be22c6c113ac", "e2bf7a6c-adf1-48da-b117-667a92874d38", "81409d63-718b-4d06-9711-7eab476b7ceb", 4, "Info", 0, "S_OK (0)", 4, "QueryInfo", "{\"Count\":1,\"Text\":\"Querycompletedsuccessfully\"}"], ["2019-02-12T10:23:02.0413963Z", "KPC.execute;57050c90-8a7d-4b29-b8d0-a40688a8185c", "dfbaa865-e29d-46e0-af17-be22c6c113ac", "e2bf7a6c-adf1-48da-b117-667a92874d38", "81409d63-718b-4d06-9711-7eab476b7ceb", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\"ExecutionTime\":0.0156475,\"resource_usage\":{\"cache\":{\"memory\":{\"hits\":0,\"misses\":0,\"total\":0},\"disk\":{\"hits\":0,\"misses\":0,\"total\":0},\"shards\":{\"hitbytes\":0,\"missbytes\":0,\"bypassbytes\":0}},\"cpu\":{\"user\":\"00: 00: 00\",\"kernel\":\"00: 00: 00\",\"totalcpu\":\"00: 00: 00\"},\"memory\":{\"peak_per_node\":0}},\"input_dataset_statistics\":{\"extents\":{\"total\":0,\"scanned\":0},\"rows\":{\"total\":0,\"scanned\":0},\"rowstores\":{\"scanned_rows\":0,\"scanned_values_size\":0}},\"dataset_statistics\":[{\"table_row_count\":0,\"table_size\":0}]}"]] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false }]azure-kusto-python-3.0.1/azure-kusto-data/tests/input/progressive_result.json000066400000000000000000000262511417222763000276770ustar00rootroot00000000000000[ { "FrameType": "DataSetHeader", "IsProgressive": true, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableKind": "QueryProperties", "TableName": "@ExtendedProperties", "Columns": [ { "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" } ], "Rows": [ [ 1, "Visualization", "{\"Visualization\":null,\"Title\":null,\"XColumn\":null,\"Series\":null,\"YColumns\":null,\"AnomalyColumns\":null,\"XTitle\":null,\"YTitle\":null,\"XAxis\":null,\"YAxis\":null,\"Legend\":null,\"YSplit\":null,\"Accumulate\":false,\"IsQuerySorted\":false,\"Kind\":null,\"Ymin\":\"NaN\",\"Ymax\":\"NaN\"}" ] ] }, { "FrameType": "TableHeader", "TableId": 1, "TableKind": "PrimaryResult", "TableName": "PrimaryResult", "Columns": [ { "ColumnName": "StartTime", "ColumnType": "datetime" }, { "ColumnName": "EndTime", "ColumnType": "datetime" }, { "ColumnName": "EpisodeId", "ColumnType": "int" }, { "ColumnName": "EventId", "ColumnType": "int" }, { "ColumnName": "State", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "string" }, { "ColumnName": "InjuriesDirect", "ColumnType": "int" }, { "ColumnName": "InjuriesIndirect", "ColumnType": "int" }, { "ColumnName": "DeathsDirect", "ColumnType": "int" }, { "ColumnName": "DeathsIndirect", "ColumnType": "int" }, { "ColumnName": "DamageProperty", "ColumnType": "int" }, { "ColumnName": "DamageCrops", "ColumnType": "int" }, { "ColumnName": "Source", "ColumnType": "string" }, { "ColumnName": "BeginLocation", "ColumnType": "string" }, { "ColumnName": "EndLocation", "ColumnType": "string" }, { "ColumnName": "BeginLat", "ColumnType": "real" }, { "ColumnName": "BeginLon", "ColumnType": "real" }, { "ColumnName": "EndLat", "ColumnType": "real" }, { "ColumnName": "EndLon", "ColumnType": "real" }, { "ColumnName": "EpisodeNarrative", "ColumnType": "string" }, { "ColumnName": "EventNarrative", "ColumnType": "string" }, { "ColumnName": "StormSummary", "ColumnType": "dynamic" } ] }, { "FrameType": "TableFragment", "TableFragmentType": "DataAppend", "TableId": 1, "Rows": [ [ "2007-01-01T00:00:00Z", "2007-01-01T00:00:00Z", 2592, 13208, "NORTH CAROLINA", "Thunderstorm Wind", 0, 0, 0, 0, 0, 0, "Public", "CASAR", "CASAR", 35.52, -81.63, 35.52, -81.63, "A small cluster of thunderstorms moved rapidly across the foothills and piedmont of western North Carolina, producing scattered wind damage.", "Several trees down.", { "TotalDamages": 0, "StartTime": "2007-01-01T00:00:00.0000000Z", "EndTime": "2007-01-01T00:00:00.0000000Z", "Details": { "Description": "Several trees down.", "Location": "NORTH CAROLINA" } } ], [ "2007-01-01T00:00:00Z", "2007-01-01T05:00:00Z", 4171, 23358, "WISCONSIN", "Winter Storm", 0, 0, 0, 0, 0, 0, "COOP Observer", "", "", null, null, null, null, "A powerful storm system moved from the southern plains through eastern Wisconsin, bringing copious amounts of precipitation to northwest Wisconsin from December 30th through the morning of January 1st. Deep tropical moisture out ahead of the storm brought heavy rains to the area on December 30th and 31st. Many areas saw over 1 inch of rain. Cold air wrapped around the system and changed the rain over to snow New Year's Eve. The snow finally ended during the early morning hours of New Year's Day, with the heaviest amounts of 6 to 12 inches reported across Ashland and Iron counties.", "", { "TotalDamages": 0, "StartTime": "2007-01-01T00:00:00.0000000Z", "EndTime": "2007-01-01T05:00:00.0000000Z", "Details": { "Description": "", "Location": "WISCONSIN" } } ], [ "2007-01-01T00:00:00Z", "2007-01-01T05:00:00Z", 4171, 23357, "WISCONSIN", "Winter Storm", 0, 0, 0, 0, 0, 0, "COOP Observer", "", "", null, null, null, null, "A powerful storm system moved from the southern plains through eastern Wisconsin, bringing copious amounts of precipitation to northwest Wisconsin from December 30th through the morning of January 1st. Deep tropical moisture out ahead of the storm brought heavy rains to the area on December 30th and 31st. Many areas saw over 1 inch of rain. Cold air wrapped around the system and changed the rain over to snow New Year's Eve. The snow finally ended during the early morning hours of New Year's Day, with the heaviest amounts of 6 to 12 inches reported across Ashland and Iron counties.", "", { "TotalDamages": 0, "StartTime": "2007-01-01T00:00:00.0000000Z", "EndTime": "2007-01-01T05:00:00.0000000Z", "Details": { "Description": "", "Location": "WISCONSIN" } } ], [ "2007-01-01T00:00:00Z", "2007-01-01T06:00:00Z", 1930, 9494, "NEW YORK", "Winter Weather", 0, 0, 0, 0, 2000, 0, "Department of Highways", "", "", null, null, null, null, "A weak area of low pressure moved across Ontario and Quebec provinces in Canada during the morning and afternoon of the 1st. Mild, moist air traveled over a seasonably cool airmass across New York and this resulted in a period of freezing rain from around Midnight to just before dawn. Freezing rain accumulated up to 1/4 of an inch across northern New York resulting in slick roads and several vehicle accidents.", "", { "TotalDamages": 2000, "StartTime": "2007-01-01T00:00:00.0000000Z", "EndTime": "2007-01-01T06:00:00.0000000Z", "Details": { "Description": "", "Location": "NEW YORK" } } ], [ "2007-01-01T00:00:00Z", "2007-01-01T06:00:00Z", 1930, 9488, "NEW YORK", "Winter Weather", 0, 0, 0, 0, 2000, 0, "Department of Highways", "", "", null, null, null, null, "A weak area of low pressure moved across Ontario and Quebec provinces in Canada during the morning and afternoon of the 1st. Mild, moist air traveled over a seasonably cool airmass across New York and this resulted in a period of freezing rain from around Midnight to just before dawn. Freezing rain accumulated up to 1/4 of an inch across northern New York resulting in slick roads and several vehicle accidents.", "", { "TotalDamages": 2000, "StartTime": "2007-01-01T00:00:00.0000000Z", "EndTime": "2007-01-01T06:00:00.0000000Z", "Details": { "Description": "", "Location": "NEW YORK" } } ] ] }, { "FrameType": "TableProgress", "TableId": 1, "TableProgress": 0.0 }, { "FrameType": "TableCompletion", "TableId": 1, "RowCount": 5 }, { "FrameType": "DataTable", "TableId": 2, "TableKind": "QueryCompletionInformation", "TableName": "QueryCompletionInformation", "Columns": [ { "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" } ], "Rows": [ [ "2021-10-17T08:28:04.3361959Z", "KPC.execute;e6ab4da1-748a-4d7e-ae35-c8b9de57d5fc", "d26c0e33-1a52-493e-931b-c3c37e4adb70", "d6e77cac-8a3c-4eff-aa3a-85b7fa59c724", "cd40ac74-c68c-4afa-aad1-992d599bc681", 4, "Info", 0, "S_OK (0)", 4, "QueryInfo", "{\"Count\":1,\"Text\":\"Query completed successfully\"}" ], [ "2021-10-17T08:28:04.3361959Z", "KPC.execute;e6ab4da1-748a-4d7e-ae35-c8b9de57d5fc", "d26c0e33-1a52-493e-931b-c3c37e4adb70", "d6e77cac-8a3c-4eff-aa3a-85b7fa59c724", "cd40ac74-c68c-4afa-aad1-992d599bc681", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\"ExecutionTime\":0.1250077,\"resource_usage\":{\"cache\":{\"memory\":{\"hits\":0,\"misses\":0,\"total\":0},\"disk\":{\"hits\":0,\"misses\":0,\"total\":0},\"shards\":{\"hot\":{\"hitbytes\":521712,\"missbytes\":0,\"retrievebytes\":0},\"cold\":{\"hitbytes\":0,\"missbytes\":0,\"retrievebytes\":0},\"bypassbytes\":0}},\"cpu\":{\"user\":\"00:00:00\",\"kernel\":\"00:00:00\",\"total cpu\":\"00:00:00\"},\"memory\":{\"peak_per_node\":1179040},\"network\":{\"inter_cluster_total_bytes\":5072,\"intra_cluster_total_bytes\":0}},\"input_dataset_statistics\":{\"extents\":{\"total\":1,\"scanned\":1,\"scanned_min_datetime\":\"2021-10-17T05:47:23.0551402Z\",\"scanned_max_datetime\":\"2021-10-17T05:47:23.0551402Z\"},\"rows\":{\"total\":59066,\"scanned\":5},\"rowstores\":{\"scanned_rows\":0,\"scanned_values_size\":0},\"shards\":{\"queries_generic\":1,\"queries_specialized\":0}},\"dataset_statistics\":[{\"table_row_count\":5,\"table_size\":3594}]}" ] ] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false } ]azure-kusto-python-3.0.1/azure-kusto-data/tests/input/query_partial_results_defer_is_false.json000066400000000000000000000060351417222763000334030ustar00rootroot00000000000000[{ "FrameType": "DataSetHeader", "IsProgressive": false, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableKind": "QueryProperties", "TableName": "@ExtendedProperties", "Columns": [{ "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" }], "Rows": [[1, "Visualization", "{\"Visualization\":null,\"Title\":null,\"XColumn\":null,\"Series\":null,\"YColumns\":null,\"AnomalyColumns\":null,\"XTitle\":null,\"YTitle\":null,\"XAxis\":null,\"YAxis\":null,\"Legend\":null,\"YSplit\":null,\"Accumulate\":false,\"IsQuerySorted\":false,\"Kind\":null}"]] }, { "FrameType": "DataTable", "TableId": 1, "TableKind": "PrimaryResult", "TableName": "PrimaryResult", "Columns": [{ "ColumnName": "x", "ColumnType": "long" }], "Rows": [[1], [2], [3], [4], [5], { "OneApiErrors": [{ "error": { "code": "LimitsExceeded", "message": "Request is invalid and cannot be executed.", "@type": "Kusto.Data.Exceptions.KustoServicePartialQueryFailureLimitsExceededException", "@message": "Query execution has exceeded the allowed limits (80DA0003): .", "@context": { "timestamp": "2018-12-10T15:10:48.8352222Z", "machineName": "RD0003FFBEDEB9", "processName": "Kusto.Azure.Svc", "processId": 4328, "threadId": 7284, "appDomainName": "RdRuntime", "clientRequestd": "KPC.execute;d3a43e37-0d7f-47a9-b6cd-a889b2aee3d3", "activityId": "a57ec272-8846-49e6-b458-460b841ed47d", "subActivityId": "a57ec272-8846-49e6-b458-460b841ed47d", "activityType": "PO-OWIN-CallContext", "parentActivityId": "a57ec272-8846-49e6-b458-460b841ed47d", "activityStack": "(Activity stack: CRID=KPC.execute;d3a43e37-0d7f-47a9-b6cd-a889b2aee3d3 ARID=a57ec272-8846-49e6-b458-460b841ed47d > PO-OWIN-CallContext/a57ec272-8846-49e6-b458-460b841ed47d)" }, "@permanent": false } }] }] }, { "FrameType": "DataSetCompletion", "HasErrors": true, "Cancelled": false, "OneApiErrors": [{ "error": { "code": "LimitsExceeded", "message": "Request is invalid and cannot be executed.", "@type": "Kusto.Data.Exceptions.KustoServicePartialQueryFailureLimitsExceededException", "@message": "Query execution has exceeded the allowed limits (80DA0003): .", "@context": { "timestamp": "2018-12-10T15:10:48.8352222Z", "machineName": "RD0003FFBEDEB9", "processName": "Kusto.Azure.Svc", "processId": 4328, "threadId": 7284, "appDomainName": "RdRuntime", "clientRequestd": "KPC.execute;d3a43e37-0d7f-47a9-b6cd-a889b2aee3d3", "activityId": "a57ec272-8846-49e6-b458-460b841ed47d", "subActivityId": "a57ec272-8846-49e6-b458-460b841ed47d", "activityType": "PO-OWIN-CallContext", "parentActivityId": "a57ec272-8846-49e6-b458-460b841ed47d", "activityStack": "(Activity stack: CRID=KPC.execute;d3a43e37-0d7f-47a9-b6cd-a889b2aee3d3 ARID=a57ec272-8846-49e6-b458-460b841ed47d > PO-OWIN-CallContext/a57ec272-8846-49e6-b458-460b841ed47d)" }, "@permanent": false } }] }]azure-kusto-python-3.0.1/azure-kusto-data/tests/input/query_partial_results_defer_is_true.json000066400000000000000000000061351417222763000332710ustar00rootroot00000000000000[{ "FrameType": "DataSetHeader", "IsProgressive": false, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableKind": "QueryProperties", "TableName": "@ExtendedProperties", "Columns": [{ "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" }], "Rows": [[1, "Visualization", "{\"Visualization\":null,\"Title\":null,\"XColumn\":null,\"Series\":null,\"YColumns\":null,\"AnomalyColumns\":null,\"XTitle\":null,\"YTitle\":null,\"XAxis\":null,\"YAxis\":null,\"Legend\":null,\"YSplit\":null,\"Accumulate\":false,\"IsQuerySorted\":false,\"Kind\":null}"]] }, { "FrameType": "DataTable", "TableId": 1, "TableKind": "PrimaryResult", "TableName": "PrimaryResult", "Columns": [{ "ColumnName": "x", "ColumnType": "long" }], "Rows": [[1], [2], [3], [4], [5]] }, { "FrameType": "DataTable", "TableId": 2, "TableKind": "QueryCompletionInformation", "TableName": "QueryCompletionInformation", "Columns": [{ "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" }], "Rows": [["2018-12-10T15:12:14.2991851Z", "KPC.execute;eba30dbd-9963-4c70-902d-60169b8b5a9c", "031555a6-3a4f-4766-b3c3-fd28bf5d8ad6", "a1e0d6fd-1def-46eb-b630-12efa5f88041", "8421d4a0-d5b8-4e04-b345-019b2b2edf85", 2, "Error", -2133196797, "Query result set too large (E_QUERY_RESULT_SET_TOO_LARGE). (-2133196797)", 1, "QueryLimitsExceeded", "{\"Count\":1,\"Text\":\"Queryresultsethasexceededtheinternalrecordcountlimit5(E_QUERY_RESULT_SET_TOO_LARGE;seehttp: //aka.ms/kustoquerylimits)\"}"], ["2018-12-10T15:12:14.2991851Z", "KPC.execute;eba30dbd-9963-4c70-902d-60169b8b5a9c", "031555a6-3a4f-4766-b3c3-fd28bf5d8ad6", "a1e0d6fd-1def-46eb-b630-12efa5f88041", "8421d4a0-d5b8-4e04-b345-019b2b2edf85", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\"ExecutionTime\":0.0,\"resource_usage\":{\"cache\":{\"memory\":{\"hits\":0,\"misses\":0,\"total\":0},\"disk\":{\"hits\":0,\"misses\":0,\"total\":0}},\"cpu\":{\"user\":\"00: 00: 00\",\"kernel\":\"00: 00: 00\",\"totalcpu\":\"00: 00: 00\"},\"memory\":{\"peak_per_node\":16777312}},\"input_dataset_statistics\":{\"extents\":{\"total\":0,\"scanned\":0},\"rows\":{\"total\":0,\"scanned\":0},\"rowstores\":{\"scanned_rows\":0,\"scanned_values_size\":0}},\"dataset_statistics\":[{\"table_row_count\":5,\"table_size\":40}]}"]] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false }]azure-kusto-python-3.0.1/azure-kusto-data/tests/input/versionshowcommandresult.json000066400000000000000000000005151417222763000311100ustar00rootroot00000000000000{"Tables":[{"TableName":"Table_0","Columns":[{"ColumnName":"BuildVersion","DataType":"String"},{"ColumnName":"BuildTime","DataType":"DateTime"},{"ColumnName":"ServiceType","DataType":"String"},{"ColumnName":"ProductVersion","DataType":"String"}],"Rows":[["1.0.6693.14577","2018-04-29T08:05:54Z","Engine","KustoMain_2018.04.29.5"]]}]}azure-kusto-python-3.0.1/azure-kusto-data/tests/input/zero_results.json000066400000000000000000000057671417222763000265020ustar00rootroot00000000000000[{ "FrameType": "DataSetHeader", "IsProgressive": false, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableKind": "QueryProperties", "TableName": "@ExtendedProperties", "Columns": [{ "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" }], "Rows": [[1, "Visualization", "{\"Visualization\":null,\"Title\":null,\"XColumn\":null,\"Series\":null,\"YColumns\":null,\"AnomalyColumns\":null,\"XTitle\":null,\"YTitle\":null,\"XAxis\":null,\"YAxis\":null,\"Legend\":null,\"YSplit\":null,\"Accumulate\":false,\"IsQuerySorted\":false,\"Kind\":null,\"Ymin\":\"NaN\",\"Ymax\":\"NaN\"}"]] }, { "FrameType": "DataTable", "TableId": 1, "TableKind": "PrimaryResult", "TableName": "PrimaryResult", "Columns": [{ "ColumnName": "print_0", "ColumnType": "string" }], "Rows": [] }, { "FrameType": "DataTable", "TableId": 2, "TableKind": "QueryCompletionInformation", "TableName": "QueryCompletionInformation", "Columns": [{ "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" }], "Rows": [["2019-02-12T10:23:02.0413963Z", "KPC.execute;57050c90-8a7d-4b29-b8d0-a40688a8185c", "dfbaa865-e29d-46e0-af17-be22c6c113ac", "e2bf7a6c-adf1-48da-b117-667a92874d38", "81409d63-718b-4d06-9711-7eab476b7ceb", 4, "Info", 0, "S_OK (0)", 4, "QueryInfo", "{\"Count\":1,\"Text\":\"Querycompletedsuccessfully\"}"], ["2019-02-12T10:23:02.0413963Z", "KPC.execute;57050c90-8a7d-4b29-b8d0-a40688a8185c", "dfbaa865-e29d-46e0-af17-be22c6c113ac", "e2bf7a6c-adf1-48da-b117-667a92874d38", "81409d63-718b-4d06-9711-7eab476b7ceb", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\"ExecutionTime\":0.0156475,\"resource_usage\":{\"cache\":{\"memory\":{\"hits\":0,\"misses\":0,\"total\":0},\"disk\":{\"hits\":0,\"misses\":0,\"total\":0},\"shards\":{\"hitbytes\":0,\"missbytes\":0,\"bypassbytes\":0}},\"cpu\":{\"user\":\"00: 00: 00\",\"kernel\":\"00: 00: 00\",\"totalcpu\":\"00: 00: 00\"},\"memory\":{\"peak_per_node\":0}},\"input_dataset_statistics\":{\"extents\":{\"total\":0,\"scanned\":0},\"rows\":{\"total\":0,\"scanned\":0},\"rowstores\":{\"scanned_rows\":0,\"scanned_values_size\":0}},\"dataset_statistics\":[{\"table_row_count\":0,\"table_size\":0}]}"]] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false }]azure-kusto-python-3.0.1/azure-kusto-data/tests/kusto_client_common.py000066400000000000000000000513201417222763000263170ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import json import os import uuid from datetime import datetime, timedelta from io import StringIO from typing import Optional, Any, Dict, Union, Iterator, Tuple import pytest from dateutil.tz import UTC from requests import HTTPError from azure.kusto.data import KustoConnectionStringBuilder from azure.kusto.data._models import KustoResultRow, KustoResultTable, KustoStreamingResultTable from azure.kusto.data.response import WellKnownDataSet, KustoStreamingResponseDataSet, KustoResponseDataSet PANDAS = False try: import pandas PANDAS = True except: pass def mocked_requests_post(*args, **kwargs): """Mock to replace requests.Session.post""" class Raw(StringIO): def __init__(self, initial_value: Optional[str]) -> None: super().__init__(initial_value, None) self.decode_content = False class MockResponse: """Mock class for KustoResponse.""" def __init__(self, json_data: Optional[Dict[str, Any]], status_code: int, url: str): self.json_data = json_data self.text = str(json_data) self.status_code = status_code self.headers = None self.reason = "" self.url = url self.raw = Raw(json.dumps(json_data)) def json(self) -> Optional[Dict[str, Any]]: """Get json data from response.""" return self.json_data def raise_for_status(self): """Raises stored :class:`HTTPError`, if one occurred.""" http_error_msg = "" if isinstance(self.reason, bytes): # We attempt to decode utf-8 first because some servers # choose to localize their reason strings. If the string # isn't utf-8, we fall back to iso-8859-1 for all other # encodings. (See PR #3538) try: reason = self.reason.decode("utf-8") except UnicodeDecodeError: reason = self.reason.decode("iso-8859-1") else: reason = self.reason if 400 <= self.status_code < 500: http_error_msg = "%s Client Error: %s for url: %s" % (self.status_code, reason, self.url) elif 500 <= self.status_code < 600: http_error_msg = "%s Server Error: %s for url: %s" % (self.status_code, reason, self.url) if http_error_msg: raise HTTPError(http_error_msg, response=self) url = args[0] if url == "https://somecluster.kusto.windows.net/v2/rest/query": if "truncationmaxrecords" in kwargs["json"]["csl"]: if json.loads(kwargs["json"]["properties"])["Options"]["deferpartialqueryfailures"]: file_name = "query_partial_results_defer_is_true.json" else: file_name = "query_partial_results_defer_is_false.json" elif "Deft" in kwargs["json"]["csl"]: file_name = "deft.json" elif "print dynamic" in kwargs["json"]["csl"]: file_name = "dynamic.json" elif "take 0" in kwargs["json"]["csl"]: file_name = "zero_results.json" elif "PrimaryResultName" in kwargs["json"]["csl"]: file_name = "null_values.json" else: raise Exception("Invalid file name") with open(os.path.join(os.path.dirname(__file__), "input", file_name), "r") as response_file: data = response_file.read() return MockResponse(json.loads(data), 200, url) elif url == "https://somecluster.kusto.windows.net/v1/rest/mgmt": if kwargs["json"]["csl"] == ".show version": file_name = "versionshowcommandresult.json" else: file_name = "adminthenquery.json" with open(os.path.join(os.path.dirname(__file__), "input", file_name), "r") as response_file: data = response_file.read() return MockResponse(json.loads(data), 200, url) elif url == "https://somecluster.kusto.windows.net/v1/rest/auth/metadata": return MockResponse( { "AzureAD": { "LoginEndpoint": "https://login.microsoftonline.com", "LoginMfaRequired": False, "KustoClientAppId": "db662dc1-0cfe-4e1c-a843-19a68e65be58", "KustoClientRedirectUri": "https://microsoft/kustoclient", "KustoServiceResourceId": "https://kusto.dev.kusto.windows.net", "FirstPartyAuthorityUrl": "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a", }, "dSTS": { "CloudEndpointSuffix": "windows.net", "DstsRealm": "realm://dsts.core.windows.net", "DstsInstance": "prod-dsts.dsts.core.windows.net", "KustoDnsHostName": "kusto.windows.net", "ServiceName": "kusto", }, }, 200, url, ) return MockResponse(None, 404, url) @pytest.fixture( params=[ "user_password", "application_key", "application_token", "device", "user_token", "managed_identity", "token_provider", "async_token_provider", "az_cli", "interactive_login", ] ) def proxy_kcsb(request) -> Tuple[KustoConnectionStringBuilder, bool]: cluster = KustoClientTestsMixin.HOST user = "test2" password = "Pa$$w0rd2" authority_id = "13456" uuid = "11111111-1111-1111-1111-111111111111" key = "key of application" token = "The app hardest token ever" return { "user_password": (KustoConnectionStringBuilder.with_aad_user_password_authentication(cluster, user, password, authority_id), True), "application_key": (KustoConnectionStringBuilder.with_aad_application_key_authentication(cluster, uuid, key, "microsoft.com"), True), "application_token": (KustoConnectionStringBuilder.with_aad_application_token_authentication(cluster, application_token=token), False), "device": (KustoConnectionStringBuilder.with_aad_device_authentication(cluster), True), "user_token": (KustoConnectionStringBuilder.with_aad_user_token_authentication(cluster, user_token=token), False), "managed_identity": (KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(cluster), False), "token_provider": (KustoConnectionStringBuilder.with_token_provider(cluster, lambda x: x), False), "async_token_provider": (KustoConnectionStringBuilder.with_async_token_provider(cluster, lambda x: x), False), "az_cli": (KustoConnectionStringBuilder.with_az_cli_authentication(cluster), True), "interactive_login": (KustoConnectionStringBuilder.with_interactive_login(cluster), True), }[request.param] DIGIT_WORDS = [str("Zero"), str("One"), str("Two"), str("Three"), str("Four"), str("Five"), str("Six"), str("Seven"), str("Eight"), str("Nine"), str("ten")] SyncResponseSet = Union[KustoStreamingResponseDataSet, KustoResponseDataSet] SyncResultTable = Union[KustoResultTable, KustoStreamingResultTable] def get_response_first_primary_result(response: SyncResponseSet) -> SyncResultTable: return next(x for x in response if x.table_kind == WellKnownDataSet.PrimaryResult) def get_table_first_row(table: SyncResultTable) -> KustoResultRow: return next(iter(table)) class KustoClientTestsMixin: HOST = "https://somecluster.kusto.windows.net" @staticmethod def _assert_client_request_id(response_args: dict, value: Optional[str] = None) -> None: header = response_args["headers"]["x-ms-client-request-id"] if value: assert header == value return [header_prefix, header_uuid] = header.split(";") assert header_prefix == "KPC.execute" uuid.UUID(header_uuid) @staticmethod def _assert_sanity_query_primary_results(results: Iterator[KustoResultRow]): expected = { "rownumber": None, "rowguid": str(""), "xdouble": None, "xfloat": None, "xbool": None, "xint16": None, "xint32": None, "xint64": None, "xuint8": None, "xuint16": None, "xuint32": None, "xuint64": None, "xdate": None, "xsmalltext": str(""), "xtext": str(""), "xnumberAsText": str(""), "xtime": None, "xtextWithNulls": str(""), "xdynamicWithNulls": str(""), } for row in results: assert row["rownumber"] == expected["rownumber"] assert row["rowguid"] == expected["rowguid"] assert row["xdouble"] == expected["xdouble"] assert row["xfloat"] == expected["xfloat"] assert row["xbool"] == expected["xbool"] assert row["xint16"] == expected["xint16"] assert row["xint32"] == expected["xint32"] assert row["xint64"] == expected["xint64"] assert row["xuint8"] == expected["xuint8"] assert row["xuint16"] == expected["xuint16"] assert row["xuint32"] == expected["xuint32"] assert row["xuint64"] == expected["xuint64"] assert row["xdate"] == expected["xdate"] assert row["xsmalltext"] == expected["xsmalltext"] assert row["xtext"] == expected["xtext"] assert row["xnumberAsText"] == expected["xnumberAsText"] assert row["xtime"] == expected["xtime"] assert row["xtextWithNulls"] == expected["xtextWithNulls"] assert row["xdynamicWithNulls"] == expected["xdynamicWithNulls"] assert isinstance(row["rownumber"], type(expected["rownumber"])) assert isinstance(row["rowguid"], type(expected["rowguid"])) assert isinstance(row["xdouble"], type(expected["xdouble"])) assert isinstance(row["xfloat"], type(expected["xfloat"])) assert isinstance(row["xbool"], type(expected["xbool"])) assert isinstance(row["xint16"], type(expected["xint16"])) assert isinstance(row["xint32"], type(expected["xint32"])) assert isinstance(row["xint64"], type(expected["xint64"])) assert isinstance(row["xuint8"], type(expected["xuint8"])) assert isinstance(row["xuint16"], type(expected["xuint16"])) assert isinstance(row["xuint32"], type(expected["xuint32"])) assert isinstance(row["xuint64"], type(expected["xuint64"])) assert isinstance(row["xdate"], type(expected["xdate"])) assert isinstance(row["xsmalltext"], type(expected["xsmalltext"])) assert isinstance(row["xtext"], type(expected["xtext"])) assert isinstance(row["xnumberAsText"], type(expected["xnumberAsText"])) assert isinstance(row["xtime"], type(expected["xtime"])) assert isinstance(row["xtextWithNulls"], type(expected["xtextWithNulls"])) assert isinstance(row["xdynamicWithNulls"], type(expected["xdynamicWithNulls"])) expected["rownumber"] = 0 if expected["rownumber"] is None else expected["rownumber"] + 1 expected["rowguid"] = str("0000000{0}-0000-0000-0001-020304050607".format(expected["rownumber"])) expected["xdouble"] = round(float(0) if expected["xdouble"] is None else expected["xdouble"] + 1.0001, 4) expected["xfloat"] = round(float(0) if expected["xfloat"] is None else expected["xfloat"] + 1.01, 2) expected["xbool"] = False if expected["xbool"] is None else not expected["xbool"] expected["xint16"] = 0 if expected["xint16"] is None else expected["xint16"] + 1 expected["xint32"] = 0 if expected["xint32"] is None else expected["xint32"] + 1 expected["xint64"] = 0 if expected["xint64"] is None else expected["xint64"] + 1 expected["xuint8"] = 0 if expected["xuint8"] is None else expected["xuint8"] + 1 expected["xuint16"] = 0 if expected["xuint16"] is None else expected["xuint16"] + 1 expected["xuint32"] = 0 if expected["xuint32"] is None else expected["xuint32"] + 1 expected["xuint64"] = 0 if expected["xuint64"] is None else expected["xuint64"] + 1 expected["xdate"] = expected["xdate"] or datetime(2013, 1, 1, 1, 1, 1, 0, tzinfo=UTC) expected["xdate"] = expected["xdate"].replace(year=expected["xdate"].year + 1) expected["xsmalltext"] = DIGIT_WORDS[int(expected["xint16"])] expected["xtext"] = DIGIT_WORDS[int(expected["xint16"])] expected["xnumberAsText"] = str(expected["xint16"]) next_time = ( timedelta() if expected["xtime"] is None else (abs(expected["xtime"]) + timedelta(days=1, seconds=1, microseconds=1000)) * (-1) ** (expected["rownumber"] + 1) ) # hacky tests - because time here is relative to previous row, after we pass a time where we have > 500 nanoseconds, # another microseconds digit is needed if expected["rownumber"] + 1 == 6: next_time += timedelta(microseconds=1) expected["xtime"] = next_time if expected["xint16"] > 0: expected["xdynamicWithNulls"] = {"rowId": expected["xint16"], "arr": [0, expected["xint16"]]} @staticmethod def _assert_sanity_query_response(response: SyncResponseSet): KustoClientTestsMixin._assert_sanity_query_primary_results(get_response_first_primary_result(response)) @staticmethod def _assert_sanity_control_command_response(response: SyncResponseSet): assert len(response) == 1 primary_table = get_response_first_primary_result(response) row_count = 0 for _ in primary_table: row_count += 1 assert row_count == 1 result = primary_table[0] assert result["BuildVersion"] == "1.0.6693.14577" assert result["BuildTime"] == datetime(year=2018, month=4, day=29, hour=8, minute=5, second=54, tzinfo=UTC) assert result["ServiceType"] == "Engine" assert result["ProductVersion"] == "KustoMain_2018.04.29.5" def _assert_sanity_data_frame_response(self, data_frame: "pandas.DataFrame"): from pandas import DataFrame, Series from pandas.testing import assert_frame_equal assert len(data_frame.columns) == 19 expected_dict = { "rownumber": Series([None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype="Int64"), "rowguid": Series( [ "", "00000000-0000-0000-0001-020304050607", "00000001-0000-0000-0001-020304050607", "00000002-0000-0000-0001-020304050607", "00000003-0000-0000-0001-020304050607", "00000004-0000-0000-0001-020304050607", "00000005-0000-0000-0001-020304050607", "00000006-0000-0000-0001-020304050607", "00000007-0000-0000-0001-020304050607", "00000008-0000-0000-0001-020304050607", "00000009-0000-0000-0001-020304050607", ], dtype=object, ), "xdouble": Series([None, 0.0, 1.0001, 2.0002, 3.0003, 4.0004, 5.0005, 6.0006, 7.0007, 8.0008, 9.0009], dtype="Float64"), "xfloat": Series([None, 0.0, 1.01, 2.02, 3.03, 4.04, 5.05, 6.06, 7.07, 8.08, 9.09], dtype="Float64"), "xbool": Series([None, False, True, False, True, False, True, False, True, False, True], dtype=bool), "xint16": Series([None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype="Int64"), "xint32": Series([None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype="Int64"), "xint64": Series([None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype="Int64"), "xuint8": Series([None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype="Int64"), "xuint16": Series([None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype="Int64"), "xuint32": Series([None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype="Int64"), "xuint64": Series([None, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype="Int64"), "xdate": Series( [ pandas.to_datetime(None), pandas.to_datetime("2014-01-01T01:01:01.0000000Z"), pandas.to_datetime("2015-01-01T01:01:01.0000001Z"), pandas.to_datetime("2016-01-01T01:01:01.0000002Z"), pandas.to_datetime("2017-01-01T01:01:01.0000003Z"), pandas.to_datetime("2018-01-01T01:01:01.0000004Z"), pandas.to_datetime("2019-01-01T01:01:01.0000005Z"), pandas.to_datetime("2020-01-01T01:01:01.0000006Z"), pandas.to_datetime("2021-01-01T01:01:01.0000007Z"), pandas.to_datetime("2022-01-01T01:01:01.0000008Z"), pandas.to_datetime("2023-01-01T01:01:01.0000009Z"), ] ), "xsmalltext": Series(["", "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"], dtype=object), "xtext": Series(["", "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"], dtype=object), "xnumberAsText": Series(["", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], dtype=object), "xtime": Series( [ "NaT", 0, "1 days 00:00:01.0010001", "-2 days 00:00:02.0020002", "3 days 00:00:03.0030003", "-4 days 00:00:04.0040004", "5 days 00:00:05.0050005", "-6 days 00:00:06.0060006", "7 days 00:00:07.0070007", "-8 days 00:00:08.0080008", "9 days 00:00:09.0090009", ], dtype="timedelta64[ns]", ), "xtextWithNulls": Series(["", "", "", "", "", "", "", "", "", "", ""], dtype=object), "xdynamicWithNulls": Series( [ str(""), str(""), {"rowId": 1, "arr": [0, 1]}, {"rowId": 2, "arr": [0, 2]}, {"rowId": 3, "arr": [0, 3]}, {"rowId": 4, "arr": [0, 4]}, {"rowId": 5, "arr": [0, 5]}, {"rowId": 6, "arr": [0, 6]}, {"rowId": 7, "arr": [0, 7]}, {"rowId": 8, "arr": [0, 8]}, {"rowId": 9, "arr": [0, 9]}, ], dtype=object, ), } columns = [ "rownumber", "rowguid", "xdouble", "xfloat", "xbool", "xint16", "xint32", "xint64", "xuint8", "xuint16", "xuint32", "xuint64", "xdate", "xsmalltext", "xtext", "xnumberAsText", "xtime", "xtextWithNulls", "xdynamicWithNulls", ] expected_data_frame = DataFrame(expected_dict, columns=columns, copy=True) assert_frame_equal(data_frame, expected_data_frame) @staticmethod def _assert_partial_results_response(response: SyncResponseSet): results = list(get_response_first_primary_result(response)) assert len(results) == 5 assert results[0]["x"] == 1 if type(response) == KustoStreamingResponseDataSet: _ = [t for t in response] # Read rest of tables assert response.errors_count == 1 assert "E_QUERY_RESULT_SET_TOO_LARGE" in response.get_exceptions()[0] assert len(response) == 3 @staticmethod def _assert_admin_then_query_response(response: SyncResponseSet): assert response.errors_count == 0 assert len(response) == 4 results = list(get_response_first_primary_result(response)) assert len(results) == 2 assert response[0].table_kind == WellKnownDataSet.PrimaryResult assert response[1].table_kind == WellKnownDataSet.QueryProperties assert response[2].table_kind == WellKnownDataSet.QueryCompletionInformation assert response[3].table_kind == WellKnownDataSet.TableOfContents @staticmethod def _assert_dynamic_response(row: KustoResultRow): assert isinstance(row[0], int) assert row[0] == 123 assert isinstance(row[1], str) assert row[1] == "123" assert isinstance(row[2], str) assert row[2] == "test bad json" assert row[3] is None assert isinstance(row[4], str) assert row[4] == '{"rowId":2,"arr":[0,2]}' assert isinstance(row[5], dict) assert row[5] == {"rowId": 2, "arr": [0, 2]} azure-kusto-python-3.0.1/azure-kusto-data/tests/sample.py000066400000000000000000000157301417222763000235320ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License """A simple example how to use KustoClient.""" from datetime import timedelta from azure.kusto.data import KustoClient, KustoConnectionStringBuilder, ClientRequestProperties from azure.kusto.data.exceptions import KustoServiceError from azure.kusto.data.helpers import dataframe_from_result_table ###################################################### ## AUTH ## ###################################################### # Note that the 'help' cluster only allows interactive # access by AAD users (and *not* AAD applications) cluster = "https://help.kusto.windows.net" # In case you want to authenticate with AAD application. client_id = "" client_secret = "" # read more at https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id authority_id = "" kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(cluster, client_id, client_secret, authority_id) # In case you want to authenticate with AAD application certificate. filename = "path to a PEM certificate" with open(filename, "r") as pem_file: PEM = pem_file.read() thumbprint = "certificate's thumbprint" kcsb = KustoConnectionStringBuilder.with_aad_application_certificate_authentication(cluster, client_id, PEM, thumbprint, authority_id) # In case you want to authenticate with AAD application certificate Subject Name & Issuer filename = "path to a PEM certificate" with open(filename, "r") as pem_file: PEM = pem_file.read() filename = "path to a public certificate" with open(filename, "r") as cert_file: public_certificate = cert_file.read() thumbprint = "certificate's thumbprint" kcsb = KustoConnectionStringBuilder.with_aad_application_certificate_sni_authentication(cluster, client_id, PEM, public_certificate, thumbprint, authority_id) # In case you want to authenticate with a System Assigned Managed Service Identity (MSI) kcsb = KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(cluster) # In case you want to authenticate with a User Assigned Managed Service Identity (MSI) user_assigned_client_id = "the AAD identity client id" kcsb = KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(cluster, client_id=user_assigned_client_id) # In case you want to authenticate with Azure CLI. # Users are required to be in a logged in state in az-cli, for this authentication method to succeed. Run `az login` to login to azure cli. kcsb = KustoConnectionStringBuilder.with_az_cli_authentication(cluster) # In case you want to authenticate with AAD username and password username = "" password = "" kcsb = KustoConnectionStringBuilder.with_aad_user_password_authentication(cluster, username, password, authority_id) # In case you want to authenticate with AAD device code. # Please note that if you choose this option, you'll need to authenticate for every new instance that is initialized. # It is highly recommended to create one instance and use it for all of your queries. kcsb = KustoConnectionStringBuilder.with_aad_device_authentication(cluster) # The authentication method will be taken from the chosen KustoConnectionStringBuilder. client = KustoClient(kcsb) ###################################################### ## QUERY ## ###################################################### # once authenticated, usage is as following db = "Samples" query = "StormEvents | take 10" response = client.execute(db, query) # iterating over rows is possible for row in response.primary_results[0]: # printing specific columns by index print("value at 0 {}".format(row[0])) print("\n") # printing specific columns by name print("EventType:{}".format(row["EventType"])) # tables are serializeable, so: with open("results.json", "w+") as f: f.write(str(response.primary_results[0])) # we also support dataframes: dataframe = dataframe_from_result_table(response.primary_results[0]) print(dataframe) # Streaming Query - rather than reading everything ahead, iterate through results as they come multiple_tables = 'StormEvents | where EventType == "Heavy Rain" | take 10; StormEvents | where EventType == "Tornado" | take 10' results = client.execute_streaming_query("DB", multiple_tables) tables_iter = results.iter_primary_results() first_table = next(tables_iter) # Will block until each row arrives for row in first_table: # printing specific columns by index print("value at 0 {}".format(row[0])) print("\n") # printing specific columns by name print("EventType:{}".format(row["EventType"])) # next(tables_iter) - throws, we can't read the next table until we exhausted this one # Read next table second_table = next(tables_iter) print(next(second_table)) # You can always access the table's properties, even after it's exhausted print(first_table.columns, first_table.table_kind, first_table.table_name) print(second_table.columns, second_table.table_kind, second_table.table_name) # Will skip forward, but the previous table will be exhausted results.set_skip_incomplete_tables(True) next(results, None) # Tables are finished # When we finish all the tables we get None print(results.tables) # Access all tables, not just the primary results ################## ### EXCEPTIONS ### ################## # Query is too big to be executed query = "StormEvents" try: response = client.execute(db, query) except KustoServiceError as error: print("2. Error:", error) print("2. Is semantic error:", error.is_semantic_error()) print("2. Has partial results:", error.has_partial_results()) print("2. Result size:", len(error.get_partial_results())) properties = ClientRequestProperties() properties.set_option(properties.results_defer_partial_query_failures_option_name, True) properties.set_option(properties.request_timeout_option_name, timedelta(seconds=8 * 60)) response = client.execute(db, query, properties=properties) print("3. Response error count: ", response.errors_count) print("3. Exceptions:", response.get_exceptions()) print("3. Result size:", len(response.primary_results)) # Query has semantic error query = "StormEvents | where foo = bar" try: response = client.execute(db, query) except KustoServiceError as error: print("4. Error:", error) print("4. Is semantic error:", error.is_semantic_error()) print("4. Has partial results:", error.has_partial_results()) client = KustoClient("https://kustolab.kusto.windows.net") response = client.execute("ML", ".show version") query = """ let max_t = datetime(2016-09-03); service_traffic | make-series num=count() on TimeStamp in range(max_t-5d, max_t, 1h) by OsVer """ response = client.execute_query("ML", query).primary_results[0] azure-kusto-python-3.0.1/azure-kusto-data/tests/test_client_request_properties.py000066400000000000000000000022701417222763000306050ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import unittest from datetime import timedelta from azure.kusto.data import ClientRequestProperties class ClientRequestPropertiesTests(unittest.TestCase): """ClientRequestProperties Tests""" def test_properties(self): """positive tests""" defer = False timeout = timedelta(seconds=10) crp = ClientRequestProperties() crp.set_option(ClientRequestProperties.results_defer_partial_query_failures_option_name, defer) crp.set_option(ClientRequestProperties.request_timeout_option_name, timeout) result = crp.to_json() assert '"{0}": false'.format(crp.results_defer_partial_query_failures_option_name) in result assert '"{0}": "0:00:10"'.format(ClientRequestProperties.request_timeout_option_name) in result assert crp.client_request_id is None assert crp.application is None assert crp.user is None crp.client_request_id = "CRID" assert crp.client_request_id == "CRID" crp.application = "myApp" assert crp.application == "myApp" crp.user = "myUser" assert crp.user == "myUser" azure-kusto-python-3.0.1/azure-kusto-data/tests/test_converter.py000066400000000000000000000050551417222763000253160ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import unittest from datetime import timedelta from azure.kusto.data._converters import to_datetime, to_timedelta class ConverterTests(unittest.TestCase): """These are unit tests that should test custom converters used in.""" def test_to_timestamp(self): """Happy path to test converter from TimeSpan to timedelta.""" # Test hours, minutes and seconds assert to_timedelta("00:00:00") == timedelta(seconds=0) assert to_timedelta("00:00:03") == timedelta(seconds=3) assert to_timedelta("00:04:03") == timedelta(minutes=4, seconds=3) assert to_timedelta("02:04:03") == timedelta(hours=2, minutes=4, seconds=3) # Test milliseconds assert to_timedelta("00:00:00.099") == timedelta(milliseconds=99) assert to_timedelta("02:04:03.0123") == timedelta(hours=2, minutes=4, seconds=3, microseconds=12300) # Test days assert to_timedelta("01.00:00:00") == timedelta(days=1) assert to_timedelta("02.04:05:07") == timedelta(days=2, hours=4, minutes=5, seconds=7) # Test negative assert to_timedelta("-01.00:00:00") == -timedelta(days=1) assert to_timedelta("-02.04:05:07") == -timedelta(days=2, hours=4, minutes=5, seconds=7) # Test all together assert to_timedelta("00.00:00:00.000") == timedelta(seconds=0) assert to_timedelta("02.04:05:07.789") == timedelta(days=2, hours=4, minutes=5, seconds=7, milliseconds=789) assert to_timedelta("03.00:00:00.111") == timedelta(days=3, milliseconds=111) # Test from Ticks assert to_timedelta(-80080008) == timedelta(microseconds=-8008001) assert to_timedelta(10010001) == timedelta(microseconds=1001000) def test_to_timestamp_fail(self): """ Sad path to test TimeSpan to timedelta converter """ self.assertRaises(ValueError, to_timedelta, "") self.assertRaises(ValueError, to_timedelta, "foo") self.assertRaises(ValueError, to_timedelta, "00") self.assertRaises(ValueError, to_timedelta, "00:00") self.assertRaises(ValueError, to_timedelta, "03.00:00:00.") self.assertRaises(ValueError, to_timedelta, "03.00:00:00.111a") def test_to_datetime(self): """Tests datetime read by KustoResultIter""" assert to_datetime("2016-06-07T16:00:00Z") is not None def test_to_datetime_fail(self): """Tests that invalid strings fails to convert to datetime""" self.assertRaises(ValueError, to_datetime, "invalid") azure-kusto-python-3.0.1/azure-kusto-data/tests/test_functional.py000066400000000000000000000144011417222763000254440ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import json import unittest from datetime import datetime, timedelta from azure.kusto.data.response import KustoResponseDataSetV2 from dateutil.tz.tz import tzutc # Sample response against all tests should be run RESPONSE_TEXT = """ [{ "FrameType": "DataSetHeader", "IsProgressive": false, "Version": "v2.0" }, { "FrameType": "DataTable", "TableId": 0, "TableName": "@ExtendedProperties", "TableKind": "QueryProperties", "Columns": [{ "ColumnName": "TableId", "ColumnType": "int" }, { "ColumnName": "Key", "ColumnType": "string" }, { "ColumnName": "Value", "ColumnType": "dynamic" }], "Rows": [[1, "Visualization", "{\\"Visualization\\":null,\\"Title\\":null,\\"XColumn\\":null,\\"Series\\":null,\\"YColumns\\":null,\\"XTitle\\":null,\\"YTitle\\":null,\\"XAxis\\":null,\\"YAxis\\":null,\\"Legend\\":null,\\"YSplit\\":null,\\"Accumulate\\":false,\\"IsQuerySorted\\":false,\\"Kind\\":null}"]] }, { "FrameType": "DataTable", "TableId": 1, "TableName": "temp", "TableKind": "PrimaryResult", "Columns": [{ "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "Name", "ColumnType": "string" }, { "ColumnName": "Altitude", "ColumnType": "long" }, { "ColumnName": "Temperature", "ColumnType": "real" }, { "ColumnName": "IsFlying", "ColumnType": "bool" }, { "ColumnName": "TimeFlying", "ColumnType": "timespan" }], "Rows": [["2016-06-06T15:35:00Z", "foo", 101, 3.14, false, 3493235670000], ["2016-06-07T16:00:00Z", "bar", 555, 2.71, true, 0], [null, "", null, null, null, null]] }, { "FrameType": "DataTable", "TableId": 2, "TableName": "QueryCompletionInformation", "TableKind": "QueryCompletionInformation", "Columns": [{ "ColumnName": "Timestamp", "ColumnType": "datetime" }, { "ColumnName": "ClientRequestId", "ColumnType": "string" }, { "ColumnName": "ActivityId", "ColumnType": "guid" }, { "ColumnName": "SubActivityId", "ColumnType": "guid" }, { "ColumnName": "ParentActivityId", "ColumnType": "guid" }, { "ColumnName": "Level", "ColumnType": "int" }, { "ColumnName": "LevelName", "ColumnType": "string" }, { "ColumnName": "StatusCode", "ColumnType": "int" }, { "ColumnName": "StatusCodeName", "ColumnType": "string" }, { "ColumnName": "EventType", "ColumnType": "int" }, { "ColumnName": "EventTypeName", "ColumnType": "string" }, { "ColumnName": "Payload", "ColumnType": "string" }], "Rows": [["2018-05-01T09:32:38.916566Z", "unspecified;e8e72755-786b-4bdc-835d-ea49d63d09fd", "5935a050-e466-48a0-991d-0ec26bd61c7e", "8182b177-7a80-4158-aca8-ff4fd8e7d3f8", "6f3c1072-2739-461c-8aa7-3cfc8ff528a8", 4, "Info", 0, "S_OK (0)", 4, "QueryInfo", "{\\"Count\\":1,\\"Text\\":\\"Querycompletedsuccessfully\\"}"], ["2018-05-01T09:32:38.916566Z", "unspecified;e8e72755-786b-4bdc-835d-ea49d63d09fd", "5935a050-e466-48a0-991d-0ec26bd61c7e", "8182b177-7a80-4158-aca8-ff4fd8e7d3f8", "6f3c1072-2739-461c-8aa7-3cfc8ff528a8", 6, "Stats", 0, "S_OK (0)", 0, "QueryResourceConsumption", "{\\"ExecutionTime\\":0.0156222,\\"resource_usage\\":{\\"cache\\":{\\"memory\\":{\\"hits\\":13,\\"misses\\":0,\\"total\\":13},\\"disk\\":{\\"hits\\":0,\\"misses\\":0,\\"total\\":0}},\\"cpu\\":{\\"user\\":\\"00: 00: 00\\",\\"kernel\\":\\"00: 00: 00\\",\\"totalcpu\\":\\"00: 00: 00\\"},\\"memory\\":{\\"peak_per_node\\":16777312}},\\"dataset_statistics\\":[{\\"table_row_count\\":3,\\"table_size\\":191}]}"]] }, { "FrameType": "DataSetCompletion", "HasErrors": false, "Cancelled": false }] """ class FunctionalTests(unittest.TestCase): """E2E tests, from part where response is received from Kusto. This does not include access to actual Kusto cluster.""" def test_valid_response(self): """Tests on happy path, validating response and iterations over it.""" response = KustoResponseDataSetV2(json.loads(RESPONSE_TEXT)) # Test that basic iteration works assert len(response) == 3 assert len(list(response.primary_results[0])) == 3 table = list(response.tables[0]) assert 1 == len(table) expected_table = [ [datetime(2016, 6, 6, 15, 35, tzinfo=tzutc()), "foo", 101, 3.14, False, timedelta(days=4, hours=1, minutes=2, seconds=3, milliseconds=567)], [datetime(2016, 6, 7, 16, tzinfo=tzutc()), "bar", 555, 2.71, True, timedelta()], [None, str(""), None, None, None, None], ] columns = ["Timestamp", "Name", "Altitude", "Temperature", "IsFlying", "TimeFlying"] # Test access by index and by column name primary_table = response.primary_results[0] for row in primary_table: # Test all types for i, expected_type in enumerate([datetime, str, int, float, bool, timedelta]): assert row[i] == row[columns[i]] assert row[i] is None or isinstance(row[i], expected_type) for row_index, row in enumerate(primary_table): expected_row = expected_table[row_index] for col_index, value in enumerate(row): assert value == expected_row[col_index] def test_invalid_table(self): """Tests calling of table with index that doesn't exists.""" response = KustoResponseDataSetV2(json.loads(RESPONSE_TEXT)) self.assertRaises(IndexError, response.__getitem__, 7) self.assertRaises(IndexError, response.__getitem__, -6) def test_column_dont_exist(self): """Tests accessing column that doesn't exists.""" response = KustoResponseDataSetV2(json.loads(RESPONSE_TEXT)) row = response.primary_results[0][0] self.assertRaises(IndexError, row.__getitem__, 10) self.assertRaises(LookupError, row.__getitem__, "NonexistentColumn") def test_iterating_after_end(self): """Tests StopIteration is raised when the response ends.""" response = KustoResponseDataSetV2(json.loads(RESPONSE_TEXT)) assert sum(1 for _ in response.primary_results[0]) == 3 def test_row_equality(self): """Tests the rows are idempotent.""" response = KustoResponseDataSetV2(json.loads(RESPONSE_TEXT)) table = response.primary_results[0] for row_index, row in enumerate(table): assert table[row_index] == row azure-kusto-python-3.0.1/azure-kusto-data/tests/test_helpers.py000066400000000000000000000064141417222763000247510ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import json import os import pytest from azure.kusto.data.helpers import dataframe_from_result_table from azure.kusto.data.response import KustoResponseDataSetV2 PANDAS = False try: import pandas import numpy PANDAS = True except: pass class TestDataFrameFromResultsTable: """Tests the dataframe_from_result_table helper function""" @pytest.mark.skipif(not PANDAS, reason="requires pandas") def test_dataframe_from_result_table(self): """Test conversion of KustoResultTable to pandas.DataFrame, including fixes for certain column types""" with open(os.path.join(os.path.dirname(__file__), "input", "dataframe.json"), "r") as response_file: data = response_file.read() response = KustoResponseDataSetV2(json.loads(data)) df = dataframe_from_result_table(response.primary_results[0]) assert df.iloc[0].RecordName == "now" assert type(df.iloc[0].RecordTime) is pandas._libs.tslibs.timestamps.Timestamp assert all(getattr(df.iloc[0].RecordTime, k) == v for k, v in {"year": 2021, "month": 12, "day": 22, "hour": 11, "minute": 43, "second": 00}.items()) assert type(df.iloc[0].RecordBool) is numpy.bool_ assert df.iloc[0].RecordBool == True assert type(df.iloc[0].RecordInt) is numpy.int64 assert df.iloc[0].RecordInt == 5678 assert type(df.iloc[0].RecordReal) is numpy.float64 assert df.iloc[0].RecordReal == 3.14159 # Kusto datetime(0000-01-01T00:00:00Z), which Pandas can't represent. assert df.iloc[1].RecordName == "earliest datetime" assert type(df.iloc[1].RecordTime) is pandas._libs.tslibs.nattype.NaTType assert pandas.isnull(df.iloc[1].RecordReal) # Kusto datetime(9999-12-31T23:59:59Z), which Pandas can't represent. assert df.iloc[2].RecordName == "latest datetime" assert type(df.iloc[2].RecordTime) is pandas._libs.tslibs.nattype.NaTType assert type(df.iloc[2].RecordReal) is numpy.float64 assert df.iloc[2].RecordReal == numpy.inf # Pandas earliest datetime assert df.iloc[3].RecordName == "earliest pandas datetime" assert type(df.iloc[3].RecordTime) is pandas._libs.tslibs.timestamps.Timestamp assert type(df.iloc[3].RecordReal) is numpy.float64 assert df.iloc[3].RecordReal == -numpy.inf # Pandas latest datetime assert df.iloc[4].RecordName == "latest pandas datetime" assert type(df.iloc[4].RecordTime) is pandas._libs.tslibs.timestamps.Timestamp # Kusto 600000000 ticks timedelta assert df.iloc[5].RecordName == "timedelta ticks" assert type(df.iloc[5].RecordTime) is pandas._libs.tslibs.timestamps.Timestamp assert type(df.iloc[5].RecordOffset) is pandas._libs.tslibs.timestamps.Timedelta assert df.iloc[5].RecordOffset == pandas.to_timedelta("00:01:00") # Kusto timedelta(1.01:01:01.0) == assert df.iloc[6].RecordName == "timedelta string" assert type(df.iloc[6].RecordTime) is pandas._libs.tslibs.timestamps.Timestamp assert type(df.iloc[6].RecordOffset) is pandas._libs.tslibs.timestamps.Timedelta assert df.iloc[6].RecordOffset == pandas.to_timedelta("1 days 01:01:01") azure-kusto-python-3.0.1/azure-kusto-data/tests/test_kusto_client.py000066400000000000000000000162731417222763000260160ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import sys import pytest from mock import patch from azure.kusto.data import KustoClient, ClientRequestProperties from azure.kusto.data._cloud_settings import CloudSettings from azure.kusto.data.exceptions import KustoMultiApiError from azure.kusto.data.helpers import dataframe_from_result_table from azure.kusto.data.response import KustoStreamingResponseDataSet from tests.kusto_client_common import KustoClientTestsMixin, mocked_requests_post, get_response_first_primary_result, get_table_first_row, proxy_kcsb PANDAS = False try: import pandas PANDAS = True except: pass @pytest.fixture(params=[KustoClient.execute_query, KustoClient.execute_streaming_query]) def method(request): return request.param class TestKustoClient(KustoClientTestsMixin): """Tests class for KustoClient API""" @patch("requests.Session.post", side_effect=mocked_requests_post) def test_sanity_query(self, mock_post, method): """Test query V2.""" client = KustoClient(self.HOST) response = method.__call__(client, "PythonTest", "Deft") self._assert_sanity_query_response(response) self._assert_client_request_id(mock_post.call_args.kwargs) @patch("requests.Session.post", side_effect=mocked_requests_post) def test_sanity_control_command(self, mock_post): """Tests contol command.""" client = KustoClient(self.HOST) response = client.execute_mgmt("NetDefaultDB", ".show version") self._assert_sanity_control_command_response(response) @pytest.mark.skipif(not PANDAS, reason="requires pandas") @patch("requests.Session.post", side_effect=mocked_requests_post) def test_sanity_data_frame(self, mock_post, method): """Tests KustoResponse to pandas.DataFrame.""" client = KustoClient(self.HOST) response = method.__call__(client, "PythonTest", "Deft") data_frame = dataframe_from_result_table(get_response_first_primary_result(response)) self._assert_sanity_data_frame_response(data_frame) @patch("requests.Session.post", side_effect=mocked_requests_post) def test_partial_results(self, mock_post, method): """Tests partial results.""" client = KustoClient(self.HOST) query = """set truncationmaxrecords = 5; range x from 1 to 10 step 1""" properties = ClientRequestProperties() properties.set_option(ClientRequestProperties.results_defer_partial_query_failures_option_name, False) with pytest.raises(KustoMultiApiError) as e: response = method.__call__(client, "PythonTest", query, properties=properties) if type(response) == KustoStreamingResponseDataSet: results = list(get_response_first_primary_result(response)) errors = e.value.get_api_errors() assert len(errors) == 1 assert errors[0].code == "LimitsExceeded" properties.set_option(ClientRequestProperties.results_defer_partial_query_failures_option_name, True) response = method.__call__(client, "PythonTest", query, properties=properties) self._assert_partial_results_response(response) @patch("requests.Session.post", side_effect=mocked_requests_post) def test_admin_then_query(self, mock_post): """Tests admin then query.""" client = KustoClient(self.HOST) query = ".show tables | project DatabaseName, TableName" response = client.execute_mgmt("PythonTest", query) self._assert_admin_then_query_response(response) @patch("requests.Session.post", side_effect=mocked_requests_post) def test_dynamic(self, mock_post, method): """Tests dynamic responses.""" client = KustoClient(self.HOST) query = """print dynamic(123), dynamic("123"), dynamic("test bad json"),""" """ dynamic(null), dynamic('{"rowId":2,"arr":[0,2]}'), dynamic({"rowId":2,"arr":[0,2]})""" row = get_table_first_row(get_response_first_primary_result(method.__call__(client, "PythonTest", query))) self._assert_dynamic_response(row) @patch("requests.Session.post", side_effect=mocked_requests_post) def test_empty_result(self, mock_post, method): """Tests dynamic responses.""" client = KustoClient(self.HOST) query = """print 'a' | take 0""" response = method.__call__(client, "PythonTest", query) assert get_response_first_primary_result(response) @patch("requests.Session.post", side_effect=mocked_requests_post) def test_null_values_in_data(self, mock_post, method): """Tests response with null values in non nullable column types""" client = KustoClient(self.HOST) query = "PrimaryResultName" response = method.__call__(client, "PythonTest", query) assert response is not None @patch("requests.Session.post", side_effect=mocked_requests_post) def test_unidentifiable_os(self, mock_post, method): """Tests unidentifiable OS doesn't fail when composing its socket options""" with patch.object(sys, "platform", "win3.1"): client = KustoClient("https://somecluster.kusto.windows.net") query = """print dynamic(123)""" row = get_table_first_row(get_response_first_primary_result(method.__call__(client, "PythonTest", query))) assert isinstance(row[0], int) @patch("requests.Session.post", side_effect=mocked_requests_post) def test_identifiable_os(self, mock_post, method): """Tests identifiable OS doesn't fail when composing its socket options""" with patch.object(sys, "platform", "win32"): client = KustoClient("https://somecluster.kusto.windows.net") query = """print dynamic(123)""" row = get_table_first_row(get_response_first_primary_result(method.__call__(client, "PythonTest", query))) assert isinstance(row[0], int) @patch("requests.Session.post", side_effect=mocked_requests_post) def test_custom_request_id(self, mock_post, method): """Test query V2.""" client = KustoClient(self.HOST) properties = ClientRequestProperties() request_id = "test_request_id" properties.client_request_id = request_id response = method.__call__(client, "PythonTest", "Deft", properties=properties) self._assert_sanity_query_response(response) self._assert_client_request_id(mock_post.call_args.kwargs, value=request_id) @patch("requests.get", side_effect=mocked_requests_post) def test_proxy_token_providers(self, mock_get, proxy_kcsb): """Test query V2.""" proxy = "https://my_proxy.sample" kcsb, auth_supports_proxy = proxy_kcsb client = KustoClient(kcsb) client.set_proxy(proxy) assert client._proxy_url == proxy expected_dict = {"http": proxy, "https": proxy} if not auth_supports_proxy: return assert client._aad_helper.token_provider._proxy_dict == expected_dict assert client._session.proxies == expected_dict CloudSettings._cloud_cache.clear() client._aad_helper.token_provider._init_resources() mock_get.assert_called_with("https://somecluster.kusto.windows.net/v1/rest/auth/metadata", proxies=expected_dict) azure-kusto-python-3.0.1/azure-kusto-data/tests/test_kusto_connection_string_builder.py000066400000000000000000000341341417222763000317670ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import unittest from uuid import uuid4 from azure.kusto.data import KustoConnectionStringBuilder class KustoConnectionStringBuilderTests(unittest.TestCase): """Tests class for KustoConnectionStringBuilder.""" PASSWORDS_REPLACEMENT = "****" def test_no_credentials(self): """Checks kcsb that is created with no credentials""" kcsbs = [ KustoConnectionStringBuilder("localhost"), KustoConnectionStringBuilder("data Source=localhost"), KustoConnectionStringBuilder("Addr=localhost"), KustoConnectionStringBuilder("Addr = localhost"), ] for kcsb in kcsbs: assert kcsb.data_source == "localhost" assert not kcsb.aad_federated_security assert kcsb.aad_user_id is None assert kcsb.password is None assert kcsb.application_client_id is None assert kcsb.application_key is None assert kcsb.authority_id == "common" assert repr(kcsb) == "Data Source=localhost;Authority Id=common" assert str(kcsb) == "Data Source=localhost;Authority Id=common" def test_aad_app(self): """Checks kcsb that is created with AAD application credentials.""" uuid = str(uuid4()) key = "key of application" kcsbs = [ KustoConnectionStringBuilder( "localhost;Application client Id={0};application Key={1};Authority Id={2} ; aad federated security = {3}".format( uuid, key, "microsoft.com", True ) ), KustoConnectionStringBuilder( "Data Source=localhost ; Application Client Id={0}; Appkey ={1};Authority Id= {2} ; aad federated security = {3}".format( uuid, key, "microsoft.com", True ) ), KustoConnectionStringBuilder( " Addr = localhost ; AppClientId = {0} ; AppKey ={1}; Authority Id={2} ; aad federated security = {3}".format(uuid, key, "microsoft.com", True) ), KustoConnectionStringBuilder( "Network Address = localhost; AppClientId = {0} ; AppKey ={1};AuthorityId={2} ; aad federated security = {3}".format( uuid, key, "microsoft.com", True ) ), KustoConnectionStringBuilder.with_aad_application_key_authentication("localhost", uuid, key, "microsoft.com"), ] try: KustoConnectionStringBuilder.with_aad_application_key_authentication("localhost", uuid, key, None) except Exception as e: # make sure error is raised when authority_id i none assert isinstance(e, ValueError) kcsb1 = KustoConnectionStringBuilder("server=localhost") kcsb1[KustoConnectionStringBuilder.ValidKeywords.application_client_id] = uuid kcsb1[KustoConnectionStringBuilder.ValidKeywords.application_key] = key kcsb1[KustoConnectionStringBuilder.ValidKeywords.authority_id] = "microsoft.com" kcsb1[KustoConnectionStringBuilder.ValidKeywords.aad_federated_security] = True kcsbs.append(kcsb1) kcsb2 = KustoConnectionStringBuilder("Server=localhost") kcsb2["AppclientId"] = uuid kcsb2["Application key"] = key kcsb2["Authority Id"] = "microsoft.com" kcsb2["aad federated security"] = True kcsbs.append(kcsb2) for kcsb in kcsbs: assert kcsb.data_source == "localhost" assert kcsb.aad_federated_security assert kcsb.aad_user_id is None assert kcsb.password is None assert kcsb.application_client_id == uuid assert kcsb.application_key == key assert kcsb.authority_id == "microsoft.com" assert repr(kcsb) == "Data Source=localhost;AAD Federated Security=True;Application Client Id={0};Application Key={1};Authority Id={2}".format( uuid, key, "microsoft.com" ) assert str(kcsb) == "Data Source=localhost;AAD Federated Security=True;Application Client Id={0};Application Key={1};Authority Id={2}".format( uuid, self.PASSWORDS_REPLACEMENT, "microsoft.com" ) def test_aad_user(self): """Checks kcsb that is created with AAD user credentials.""" user = "test" password = "Pa$$w0rd" kcsbs = [ KustoConnectionStringBuilder("localhost;AAD User ID={0};password={1} ;AAD Federated Security=True ".format(user, password)), KustoConnectionStringBuilder("Data Source=localhost ; AaD User ID={0}; Password ={1} ;AAD Federated Security=True".format(user, password)), KustoConnectionStringBuilder(" Addr = localhost ; AAD User ID = {0} ; Pwd ={1} ;AAD Federated Security=True".format(user, password)), KustoConnectionStringBuilder("Network Address = localhost; AAD User iD = {0} ; Pwd = {1} ;AAD Federated Security= True ".format(user, password)), KustoConnectionStringBuilder.with_aad_user_password_authentication("localhost", user, password), ] kcsb1 = KustoConnectionStringBuilder("Server=localhost") kcsb1[KustoConnectionStringBuilder.ValidKeywords.aad_user_id] = user kcsb1[KustoConnectionStringBuilder.ValidKeywords.password] = password kcsb1[KustoConnectionStringBuilder.ValidKeywords.aad_federated_security] = True kcsbs.append(kcsb1) kcsb2 = KustoConnectionStringBuilder("server=localhost") kcsb2["AAD User ID"] = user kcsb2["Password"] = password kcsb2["aad federated security"] = True kcsbs.append(kcsb2) for kcsb in kcsbs: assert kcsb.data_source == "localhost" assert kcsb.aad_federated_security assert kcsb.aad_user_id == user assert kcsb.password == password assert kcsb.application_client_id is None assert kcsb.application_key is None assert kcsb.authority_id == "common" assert repr(kcsb) == "Data Source=localhost;AAD Federated Security=True;AAD User ID={0};Password={1};Authority Id=common".format(user, password) assert str(kcsb) == "Data Source=localhost;AAD Federated Security=True;AAD User ID={0};Password={1};Authority Id=common".format( user, self.PASSWORDS_REPLACEMENT ) def test_aad_user_with_authority(self): """Checks kcsb that is created with AAD user credentials.""" user = "test2" password = "Pa$$w0rd2" authority_id = "13456" kcsb = KustoConnectionStringBuilder.with_aad_user_password_authentication("localhost", user, password, authority_id) assert kcsb.data_source == "localhost" assert kcsb.aad_federated_security assert kcsb.aad_user_id == user assert kcsb.password == password assert kcsb.application_client_id is None assert kcsb.application_key is None assert kcsb.authority_id == authority_id assert repr(kcsb) == "Data Source=localhost;AAD Federated Security=True;AAD User ID={0};Password={1};Authority Id=13456".format(user, password) assert str(kcsb) == "Data Source=localhost;AAD Federated Security=True;AAD User ID={0};Password={1};Authority Id=13456".format( user, self.PASSWORDS_REPLACEMENT ) def test_aad_device_login(self): """Checks kcsb that is created with AAD device login.""" kcsb = KustoConnectionStringBuilder.with_aad_device_authentication("localhost") assert kcsb.data_source == "localhost" assert kcsb.aad_federated_security assert kcsb.aad_user_id is None assert kcsb.password is None assert kcsb.application_client_id is None assert kcsb.application_key is None assert kcsb.authority_id == "common" assert repr(kcsb) == "Data Source=localhost;AAD Federated Security=True;Authority Id=common" assert str(kcsb) == "Data Source=localhost;AAD Federated Security=True;Authority Id=common" def test_aad_app_token(self): """Checks kcsb that is created with AAD user token.""" token = "The app hardest token ever" kcsb = KustoConnectionStringBuilder.with_aad_application_token_authentication("localhost", application_token=token) assert kcsb.data_source == "localhost" assert kcsb.application_token == token assert kcsb.aad_federated_security assert kcsb.aad_user_id is None assert kcsb.password is None assert kcsb.application_client_id is None assert kcsb.application_key is None assert kcsb.user_token is None assert kcsb.authority_id == "common" assert repr(kcsb) == "Data Source=localhost;AAD Federated Security=True;Authority Id=common;Application Token=%s" % token assert str(kcsb) == "Data Source=localhost;AAD Federated Security=True;Authority Id=common;Application Token=%s" % self.PASSWORDS_REPLACEMENT def test_aad_user_token(self): """Checks kcsb that is created with AAD user token.""" token = "The user hardest token ever" kcsb = KustoConnectionStringBuilder.with_aad_user_token_authentication("localhost", user_token=token) assert kcsb.data_source == "localhost" assert kcsb.user_token == token assert kcsb.aad_federated_security assert kcsb.aad_user_id is None assert kcsb.password is None assert kcsb.application_client_id is None assert kcsb.application_key is None assert kcsb.application_token is None assert kcsb.authority_id == "common" assert repr(kcsb) == "Data Source=localhost;AAD Federated Security=True;Authority Id=common;User Token=%s" % token assert str(kcsb) == "Data Source=localhost;AAD Federated Security=True;Authority Id=common;User Token=%s" % self.PASSWORDS_REPLACEMENT def test_add_msi(self): client_guid = "kjhjk" object_guid = "87687687" res_guid = "kajsdghdijewhag" """ Use of object_id and msi_res_id is disabled pending support of azure-identity When version 1.4.1 is released and these parameters are supported enable the functionality and tests back """ exception_occurred = False try: KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication("localhost2", object_id=object_guid, timeout=3) except ValueError: exception_occurred = True assert exception_occurred is True exception_occurred = False try: KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication("localhost3", msi_res_id=res_guid) except ValueError: exception_occurred = True assert exception_occurred is True kcsb = [ KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication("localhost0", timeout=1), KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication("localhost1", client_id=client_guid, timeout=2), # KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication("localhost2", object_id=object_guid, timeout=3), # KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication("localhost3", msi_res_id=res_guid), ] assert kcsb[0].msi_authentication assert kcsb[0].msi_parameters["connection_timeout"] == 1 assert "client_id" not in kcsb[0].msi_parameters assert "object_id" not in kcsb[0].msi_parameters assert "msi_res_id" not in kcsb[0].msi_parameters assert kcsb[1].msi_authentication assert kcsb[1].msi_parameters["connection_timeout"] == 2 assert kcsb[1].msi_parameters["client_id"] == client_guid assert "object_id" not in kcsb[1].msi_parameters assert "msi_res_id" not in kcsb[1].msi_parameters """ assert kcsb[2].msi_authentication assert kcsb[2].msi_parameters["connection_timeout"] == 3 assert "client_id" not in kcsb[2].msi_parameters assert kcsb[2].msi_parameters["object_id"] == object_guid assert "msi_res_id" not in kcsb[2].msi_parameters assert kcsb[3].msi_authentication assert "timeout" not in kcsb[3].msi_parameters assert "client_id" not in kcsb[3].msi_parameters assert "object_id" not in kcsb[3].msi_parameters assert kcsb[3].msi_parameters["msi_res_id"] == res_guid """ exception_occurred = False try: fault = KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication("localhost", client_id=client_guid, object_id=object_guid) except ValueError as e: exception_occurred = True finally: assert exception_occurred def test_add_token_provider(self): caller_token = "caller token" token_provider = lambda: caller_token kscb = KustoConnectionStringBuilder.with_token_provider("localhost", token_provider) assert kscb.token_provider() == caller_token exception_occurred = False try: kscb = KustoConnectionStringBuilder.with_token_provider("localhost", caller_token) except AssertionError as ex: exception_occurred = True finally: assert exception_occurred def test_add_async_token_provider(self): caller_token = "caller token" async def async_token_provider(): return caller_token kscb = KustoConnectionStringBuilder.with_async_token_provider("localhost", async_token_provider) assert kscb.async_token_provider() is not None exception_occurred = False try: kscb = KustoConnectionStringBuilder.with_async_token_provider("localhost", caller_token) except AssertionError as ex: exception_occurred = True finally: assert exception_occurred azure-kusto-python-3.0.1/azure-kusto-data/tests/test_models.py000066400000000000000000000006471417222763000245740ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import json import os from azure.kusto.data._models import KustoResultTable def test_str_and_dates_smoke(): with open(os.path.join(os.path.dirname(__file__), "input", "deft.json"), "r") as f: data = f.read() json_table = json.loads(data)[2] result_table = KustoResultTable(json_table) assert len(str(result_table)) == 4537 azure-kusto-python-3.0.1/azure-kusto-data/tests/test_security.py000066400000000000000000000131411417222763000251510ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import pytest from azure.kusto.data import KustoConnectionStringBuilder from azure.kusto.data._token_providers import * from azure.kusto.data.exceptions import KustoAuthenticationError from azure.kusto.data.security import _AadHelper KUSTO_TEST_URI = "https://thisclusterdoesnotexist.kusto.windows.net" TEST_INTERACTIVE_AUTH = False # User interaction required, enable this when running test manually CloudSettings._cloud_cache[KUSTO_TEST_URI] = CloudSettings.DEFAULT_CLOUD CloudSettings._cloud_cache["https://somecluster.kusto.windows.net"] = CloudSettings.DEFAULT_CLOUD def test_unauthorized_exception(): """Test the exception thrown when authorization fails.""" cluster = "https://somecluster.kusto.windows.net" username = "username@microsoft.com" kcsb = KustoConnectionStringBuilder.with_aad_user_password_authentication(cluster, username, "StrongestPasswordEver", "authorityName") aad_helper = _AadHelper(kcsb, False) aad_helper.token_provider._init_resources() try: aad_helper.acquire_authorization_header() except KustoAuthenticationError as error: assert error.authentication_method == UserPassTokenProvider.name() assert error.authority == "https://login.microsoftonline.com/authorityName" assert error.kusto_cluster == cluster assert error.kwargs["username"] == username assert error.kwargs["client_id"] == CloudSettings.DEFAULT_CLOUD.kusto_client_app_id # TODO: remove this once we can control the timeout @pytest.mark.skip() def test_msi_auth(): """ * * * Note * * * Each connection test takes about 15-20 seconds which is the time it takes TCP to fail connecting to the nonexistent MSI endpoint The timeout option does not seem to affect this behavior. Could be it only affects the waiting time fora response in successful connections. Please be prudent in adding any future tests! """ client_guid = "kjhjk" object_guid = "87687687" res_guid = "kajsdghdijewhag" """ Use of object_id and msi_res_id is disabled pending support of azure-identity When version 1.4.1 is released and these parameters are supported enable the functionality and tests back """ kcsb = [ KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(KUSTO_TEST_URI, timeout=1), KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(KUSTO_TEST_URI, client_id=client_guid, timeout=1), # KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(KUSTO_TEST_URI, object_id=object_guid, timeout=1), # KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(KUSTO_TEST_URI, msi_res_id=res_guid, timeout=1), ] helpers = [_AadHelper(i, False) for i in kcsb] for h in helpers: h.token_provider._init_resources() try: helpers[0].acquire_authorization_header() except KustoAuthenticationError as e: assert e.authentication_method == MsiTokenProvider.name() assert "client_id" not in e.kwargs assert "object_id" not in e.kwargs assert "msi_res_id" not in e.kwargs try: helpers[1].acquire_authorization_header() except KustoAuthenticationError as e: assert e.authentication_method == MsiTokenProvider.name() assert e.kwargs["client_id"] == client_guid assert "object_id" not in e.kwargs assert "msi_res_id" not in e.kwargs assert str(e.exception).index("client_id") > -1 assert str(e.exception).index(client_guid) > -1 def test_token_provider_auth(): valid_token_provider = lambda: "caller token" invalid_token_provider = lambda: 12345678 valid_kcsb = KustoConnectionStringBuilder.with_token_provider(KUSTO_TEST_URI, valid_token_provider) invalid_kcsb = KustoConnectionStringBuilder.with_token_provider(KUSTO_TEST_URI, invalid_token_provider) valid_helper = _AadHelper(valid_kcsb, False) valid_helper.token_provider._init_resources() invalid_helper = _AadHelper(invalid_kcsb, False) invalid_helper.token_provider._init_resources() auth_header = valid_helper.acquire_authorization_header() assert auth_header.index(valid_token_provider()) > -1 try: invalid_helper.acquire_authorization_header() except KustoAuthenticationError as e: assert e.authentication_method == CallbackTokenProvider.name() assert str(e.exception).index(str(type(invalid_token_provider()))) > -1 def test_user_app_token_auth(): token = "123456446" user_kcsb = KustoConnectionStringBuilder.with_aad_user_token_authentication(KUSTO_TEST_URI, token) app_kcsb = KustoConnectionStringBuilder.with_aad_application_token_authentication(KUSTO_TEST_URI, token) user_helper = _AadHelper(user_kcsb, False) app_helper = _AadHelper(app_kcsb, False) user_helper.token_provider._init_resources() app_helper.token_provider._init_resources() auth_header = user_helper.acquire_authorization_header() assert auth_header.index(token) > -1 auth_header = app_helper.acquire_authorization_header() assert auth_header.index(token) > -1 def test_interactive_login(): if not TEST_INTERACTIVE_AUTH: print(" *** Skipped interactive login Test ***") return kcsb = KustoConnectionStringBuilder.with_interactive_login(KUSTO_TEST_URI) aad_helper = _AadHelper(kcsb, False) # should prompt header = aad_helper.acquire_authorization_header() assert header is not None # should not prompt header = aad_helper.acquire_authorization_header() assert header is not None azure-kusto-python-3.0.1/azure-kusto-data/tests/test_streaming_query.py000066400000000000000000000317121417222763000265240ustar00rootroot00000000000000import os from io import StringIO import pytest from azure.kusto.data._models import WellKnownDataSet, KustoResultRow, KustoResultColumn from azure.kusto.data.aio.response import KustoStreamingResponseDataSet as AsyncKustoStreamingResponseDataSet from azure.kusto.data.aio.streaming_response import JsonTokenReader as AsyncJsonTokenReader, StreamingDataSetEnumerator as AsyncProgressiveDataSetEnumerator from azure.kusto.data.exceptions import KustoServiceError, KustoStreamingQueryError, KustoTokenParsingError, KustoUnsupportedApiError, KustoMultiApiError from azure.kusto.data.response import KustoStreamingResponseDataSet from azure.kusto.data.streaming_response import JsonTokenReader, StreamingDataSetEnumerator, FrameType, JsonTokenType from tests.kusto_client_common import KustoClientTestsMixin class MockAioFile: def __init__(self, filename): self.filename = filename def __enter__(self): self.file = open(self.filename, "rb") return self def __exit__(self, exc_type, exc_val, exc_tb): self.file.close() async def read(self, n=-1): return self.file.read(n) class AsyncStringIO: def __init__(self, string): self.string_io = StringIO(string) async def read(self, n=-1): return self.string_io.read(n) class TestStreamingQuery(KustoClientTestsMixin): """Tests class for KustoClient API""" @staticmethod def open_json_file(file_name: str): return open(os.path.join(os.path.dirname(__file__), "input", file_name), "rb") @staticmethod def open_async_json_file(file_name: str): return MockAioFile(os.path.join(os.path.dirname(__file__), "input", file_name)) def test_sanity(self): with self.open_json_file("deft.json") as f: reader = StreamingDataSetEnumerator(JsonTokenReader(f)) for i in reader: if i["FrameType"] == FrameType.DataTable and i["TableKind"] == WellKnownDataSet.PrimaryResult.value: columns = [KustoResultColumn(column, index) for index, column in enumerate(i["Columns"])] self._assert_sanity_query_primary_results(KustoResultRow(columns, r) for r in i["Rows"]) def test_alternative_order(self): with self.open_json_file("alternative_order.json") as f: reader = StreamingDataSetEnumerator(JsonTokenReader(f)) for i in reader: if i["FrameType"] == FrameType.DataTable and i["TableKind"] == WellKnownDataSet.PrimaryResult.value: columns = [KustoResultColumn(column, index) for index, column in enumerate(i["Columns"])] self._assert_sanity_query_primary_results(KustoResultRow(columns, r) for r in i["Rows"]) def test_progressive_unsupported(self): with self.open_json_file("progressive_result.json") as f: reader = StreamingDataSetEnumerator(JsonTokenReader(f)) with pytest.raises(KustoUnsupportedApiError): for _ in reader: pass with self.open_json_file("deft_with_progressive_result.json") as f: reader = StreamingDataSetEnumerator(JsonTokenReader(f)) with pytest.raises(KustoUnsupportedApiError): for _ in reader: pass def test_dynamic(self): with self.open_json_file("dynamic.json") as f: reader = StreamingDataSetEnumerator(JsonTokenReader(f)) for i in reader: if i["FrameType"] == FrameType.DataTable and i["TableKind"] == WellKnownDataSet.PrimaryResult.value: row = next(i["Rows"]) self._assert_dynamic_response(row) def test_sanity_kusto_streaming_response_dataset(self): with self.open_json_file("deft.json") as f: reader = StreamingDataSetEnumerator(JsonTokenReader(f)) response = KustoStreamingResponseDataSet(reader) primary_tables = response.iter_primary_results() # Before reading all of the tables these results won't be available with pytest.raises(KustoStreamingQueryError): errors_count = response.errors_count with pytest.raises(KustoStreamingQueryError): exceptions = response.get_exceptions() first_table = next(primary_tables) # Can't advance by default until current table is finished with pytest.raises(KustoStreamingQueryError): next(primary_tables) self._assert_sanity_query_primary_results(first_table) assert next(primary_tables, None) is None assert response.errors_count == 0 assert response.get_exceptions() == [] assert response.finished def test_exception_in_row(self): with self.open_json_file("query_partial_results_defer_is_false.json") as f: reader = StreamingDataSetEnumerator(JsonTokenReader(f)) response = KustoStreamingResponseDataSet(reader) table = next(response.iter_primary_results()) with pytest.raises(KustoServiceError): rows = [r for r in table] @pytest.mark.asyncio async def test_sanity_async(self): with self.open_async_json_file("deft.json") as f: reader = AsyncProgressiveDataSetEnumerator(AsyncJsonTokenReader(f)) async for i in reader: if i["FrameType"] == FrameType.DataTable and i["TableKind"] == WellKnownDataSet.PrimaryResult.value: columns = [KustoResultColumn(column, index) for index, column in enumerate(i["Columns"])] rows = [KustoResultRow(columns, r) async for r in i["Rows"]] self._assert_sanity_query_primary_results(rows) @pytest.mark.asyncio async def test_alternative_order_async(self): with self.open_async_json_file("alternative_order.json") as f: reader = AsyncProgressiveDataSetEnumerator(AsyncJsonTokenReader(f)) async for i in reader: if i["FrameType"] == FrameType.DataTable and i["TableKind"] == WellKnownDataSet.PrimaryResult.value: columns = [KustoResultColumn(column, index) for index, column in enumerate(i["Columns"])] rows = [KustoResultRow(columns, r) async for r in i["Rows"]] self._assert_sanity_query_primary_results(rows) @pytest.mark.asyncio async def test_progressive_unsupported_async(self): with self.open_async_json_file("progressive_result.json") as f: reader = AsyncProgressiveDataSetEnumerator(AsyncJsonTokenReader(f)) with pytest.raises(KustoUnsupportedApiError): async for _ in reader: pass with self.open_async_json_file("deft_with_progressive_result.json") as f: reader = AsyncProgressiveDataSetEnumerator(AsyncJsonTokenReader(f)) with pytest.raises(KustoUnsupportedApiError): async for _ in reader: pass @pytest.mark.asyncio async def test_dynamic_async(self): with self.open_async_json_file("dynamic.json") as f: reader = AsyncProgressiveDataSetEnumerator(AsyncJsonTokenReader(f)) async for i in reader: if i["FrameType"] == FrameType.DataTable and i["TableKind"] == WellKnownDataSet.PrimaryResult.value: row = await i["Rows"].__anext__() self._assert_dynamic_response(row) @pytest.mark.asyncio async def test_sanity_kusto_streaming_response_dataset_async(self): with self.open_async_json_file("deft.json") as f: reader = AsyncProgressiveDataSetEnumerator(AsyncJsonTokenReader(f)) response = AsyncKustoStreamingResponseDataSet(reader) primary_tables = response.iter_primary_results() # Before reading all of the tables these results won't be available with pytest.raises(KustoStreamingQueryError): errors_count = response.errors_count with pytest.raises(KustoStreamingQueryError): exceptions = response.get_exceptions() first_table = await primary_tables.__anext__() # Can't advance by default until current table is finished with pytest.raises(KustoStreamingQueryError): await primary_tables.__anext__() self._assert_sanity_query_primary_results([x async for x in first_table]) with pytest.raises(StopAsyncIteration): await primary_tables.__anext__() assert response.errors_count == 0 assert response.get_exceptions() == [] assert response.finished @pytest.mark.asyncio async def test_exception_in_row_async(self): with self.open_async_json_file("query_partial_results_defer_is_false.json") as f: reader = AsyncProgressiveDataSetEnumerator(AsyncJsonTokenReader(f)) response = AsyncKustoStreamingResponseDataSet(reader) table = await response.iter_primary_results().__anext__() with pytest.raises(KustoMultiApiError): rows = [r async for r in table] class TestJsonTokenReader: def get_reader(self, data) -> JsonTokenReader: return JsonTokenReader(StringIO(data)) def get_async_reader(self, data) -> AsyncJsonTokenReader: return AsyncJsonTokenReader(AsyncStringIO(data)) def test_reading_token(self): reader = self.get_reader("{") assert reader.read_next_token_or_throw().token_type == JsonTokenType.START_MAP with pytest.raises(KustoTokenParsingError): reader.read_next_token_or_throw() def test_read_token_of_type(self): reader = self.get_reader("{}") assert reader.read_token_of_type(JsonTokenType.START_MAP).token_type == JsonTokenType.START_MAP with pytest.raises(KustoTokenParsingError): reader.read_token_of_type(JsonTokenType.START_MAP) def test_types(self): reader = self.get_reader('{"junk": [[[]]], "key": ["a",false,3.63]}') assert reader.read_start_object().token_type == JsonTokenType.START_MAP assert reader.skip_until_property_name("key").token_type == JsonTokenType.MAP_KEY assert reader.read_start_array().token_type == JsonTokenType.START_ARRAY assert reader.read_string() == "a" assert reader.read_boolean() is False assert reader.read_number() == 3.63 def test_skip(self): reader = self.get_reader('{"junk": [[[]]], "key": "a", "junk2": {"key2": "aaaaa"}, "key2": "www"}') assert reader.read_start_object().token_type == JsonTokenType.START_MAP key = reader.skip_until_any_property_name("key", "junk2") assert key.token_type == JsonTokenType.MAP_KEY assert reader.read_string() == "a" key2 = reader.skip_until_property_name_or_end_object("key2") assert key2.token_type == JsonTokenType.MAP_KEY assert key2.token_path == "" assert reader.read_string() == "www" assert reader.skip_until_property_name_or_end_object().token_type == JsonTokenType.END_MAP @pytest.mark.asyncio async def test_reading_token_async(self): reader = self.get_async_reader("{") assert (await reader.read_next_token_or_throw()).token_type == JsonTokenType.START_MAP with pytest.raises(KustoTokenParsingError): await reader.read_next_token_or_throw() @pytest.mark.asyncio async def test_read_token_of_type_async(self): reader = self.get_async_reader("{}") assert (await reader.read_token_of_type(JsonTokenType.START_MAP)).token_type == JsonTokenType.START_MAP with pytest.raises(KustoTokenParsingError): await reader.read_token_of_type(JsonTokenType.START_MAP) @pytest.mark.asyncio async def test_types_async(self): reader = self.get_async_reader('{"junk": [[[]]], "key": ["a",false,3.63]}') assert (await reader.read_start_object()).token_type == JsonTokenType.START_MAP assert (await reader.skip_until_property_name("key")).token_type == JsonTokenType.MAP_KEY assert (await reader.read_start_array()).token_type == JsonTokenType.START_ARRAY assert (await reader.read_string()) == "a" assert (await reader.read_boolean()) is False assert (await reader.read_number()) == 3.63 @pytest.mark.asyncio async def test_skip_async(self): reader = self.get_async_reader('{"junk": [[[]]], "key": "a", "junk2": {"key2": "aaaaa"}, "key2": "www" }') assert (await reader.read_start_object()).token_type == JsonTokenType.START_MAP key = await reader.skip_until_any_property_name("key", "junk2") assert key.token_type == JsonTokenType.MAP_KEY assert (await reader.read_string()) == "a" key2 = await reader.skip_until_property_name_or_end_object("key2") assert key2.token_type == JsonTokenType.MAP_KEY assert key2.token_path == "" assert (await reader.read_string()) == "www" assert (await reader.skip_until_property_name_or_end_object()).token_type == JsonTokenType.END_MAP azure-kusto-python-3.0.1/azure-kusto-data/tests/test_token_providers.py000066400000000000000000000331071417222763000265230ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import os import unittest from threading import Thread from asgiref.sync import async_to_sync from azure.kusto.data._cloud_settings import CloudInfo from azure.kusto.data._token_providers import * KUSTO_URI = "https://sdkse2etest.eastus.kusto.windows.net" TOKEN_VALUE = "little miss sunshine" TEST_AZ_AUTH = False # enable this in environments with az cli installed, and make sure to call 'az login' first TEST_MSI_AUTH = False # enable this in environments with MSI enabled and make sure to set the relevant environment variables TEST_DEVICE_AUTH = False # User interaction required, enable this when running test manually TEST_INTERACTIVE_AUTH = False # User interaction required, enable this when running test manually class MockProvider(TokenProviderBase): def __init__(self, is_async: bool = False): super().__init__(is_async) self._silent_token = False self.init_count = 0 @staticmethod def name() -> str: return "MockProvider" def _context_impl(self) -> dict: return {"authority": "MockProvider"} def _init_impl(self): self.init_count = self.init_count + 1 def _get_token_impl(self) -> Optional[dict]: self._silent_token = True return {TokenConstants.MSAL_ACCESS_TOKEN: "token"} def _get_token_from_cache_impl(self) -> Optional[dict]: if self._silent_token: return {TokenConstants.MSAL_ACCESS_TOKEN: "token"} return None class TokenProviderTests(unittest.TestCase): @staticmethod def test_base_provider(): # test init with no URI provider = MockProvider() # Test provider with URI, No silent token provider = MockProvider() token = provider._get_token_from_cache_impl() assert provider.init_count == 0 assert token is None token = provider.get_token() assert provider.init_count == 1 assert TokenConstants.MSAL_ACCESS_TOKEN in token token = provider._get_token_from_cache_impl() assert TokenConstants.MSAL_ACCESS_TOKEN in token token = provider.get_token() assert provider.init_count == 1 good_token = {TokenConstants.MSAL_ACCESS_TOKEN: TOKEN_VALUE} bad_token1 = None bad_token2 = {"error": "something bad occurred"} assert provider._valid_token_or_none(good_token) == good_token assert provider._valid_token_or_none(bad_token1) is None assert provider._valid_token_or_none(bad_token2) is None assert provider._valid_token_or_throw(good_token) == good_token exception_occurred = False try: provider._valid_token_or_throw(bad_token1) except KustoClientError: exception_occurred = True finally: assert exception_occurred exception_occurred = False try: provider._valid_token_or_throw(bad_token2) except KustoClientError: exception_occurred = True finally: assert exception_occurred @staticmethod def get_token_value(token: dict): assert token is not None assert TokenConstants.MSAL_ERROR not in token value = None if TokenConstants.MSAL_ACCESS_TOKEN in token: return token[TokenConstants.MSAL_ACCESS_TOKEN] elif TokenConstants.AZ_ACCESS_TOKEN in token: return token[TokenConstants.AZ_ACCESS_TOKEN] else: assert False @staticmethod def test_fail_async_call(): provider = BasicTokenProvider(token=TOKEN_VALUE) try: async_to_sync(provider.get_token_async)() assert False, "Expected KustoAsyncUsageError to occur" except KustoAsyncUsageError as e: assert str(e) == "Method get_token_async can't be called from a synchronous client" try: async_to_sync(provider.context_async)() assert False, "Expected KustoAsyncUsageError to occur" except KustoAsyncUsageError as e: assert str(e) == "Method context_async can't be called from a synchronous client" @staticmethod def test_basic_provider(): provider = BasicTokenProvider(token=TOKEN_VALUE) token = provider.get_token() assert TokenProviderTests.get_token_value(token) == TOKEN_VALUE @staticmethod def test_basic_provider_in_thread(): exc = [] def inner(exc): try: TokenProviderTests.test_basic_provider() except Exception as e: exc.append(e) pass t = Thread(target=inner, args=(exc,)) t.start() t.join() if exc: raise exc[0] @staticmethod def test_callback_token_provider(): provider = CallbackTokenProvider(token_callback=lambda: TOKEN_VALUE, async_token_callback=None) token = provider.get_token() assert TokenProviderTests.get_token_value(token) == TOKEN_VALUE provider = CallbackTokenProvider(token_callback=lambda: 0, async_token_callback=None) # token is not a string exception_occurred = False try: provider.get_token() except KustoClientError: exception_occurred = True finally: assert exception_occurred @staticmethod def test_az_provider(): if not TEST_AZ_AUTH: print(" *** Skipped Az-Cli Provider Test ***") return print("Note!\nThe test 'test_az_provider' will fail if 'az login' was not called.") provider = AzCliTokenProvider(KUSTO_URI) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None # another run to pass through the cache token = provider._get_token_from_cache_impl() assert TokenProviderTests.get_token_value(token) is not None @staticmethod def test_msi_provider(): if not TEST_MSI_AUTH: print(" *** Skipped MSI Provider Test ***") return user_msi_object_id = os.environ.get("MSI_OBJECT_ID") user_msi_client_id = os.environ.get("MSI_CLIENT_ID") # system MSI provider = MsiTokenProvider(KUSTO_URI) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None if user_msi_object_id is not None: args = {"object_id": user_msi_object_id} provider = MsiTokenProvider(KUSTO_URI, args) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None else: print(" *** Skipped MSI Provider Client Id Test ***") if user_msi_client_id is not None: args = {"client_id": user_msi_client_id} provider = MsiTokenProvider(KUSTO_URI, args) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None else: print(" *** Skipped MSI Provider Object Id Test ***") @staticmethod def test_user_pass_provider(): username = os.environ.get("USER_NAME") password = os.environ.get("USER_PASS") auth = os.environ.get("USER_AUTH_ID", "organizations") if username and password and auth: provider = UserPassTokenProvider(KUSTO_URI, auth, username, password) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert TokenProviderTests.get_token_value(token) is not None else: print(" *** Skipped User & Pass Provider Test ***") @staticmethod def test_device_auth_provider(): if not TEST_DEVICE_AUTH: print(" *** Skipped User Device Flow Test ***") return def callback(x): # break here if you debug this test, and get the code from 'x' print(x) provider = DeviceLoginTokenProvider(KUSTO_URI, "organizations", callback) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert TokenProviderTests.get_token_value(token) is not None @staticmethod def test_interactive_login(): if not TEST_INTERACTIVE_AUTH: print(" *** Skipped interactive login Test ***") return auth_id = os.environ.get("APP_AUTH_ID", "72f988bf-86f1-41af-91ab-2d7cd011db47") provider = InteractiveLoginTokenProvider(KUSTO_URI, auth_id) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert TokenProviderTests.get_token_value(token) is not None @staticmethod def test_app_key_provider(): # default details are for kusto-client-e2e-test-app # to run the test, get the key from Azure portal app_id = os.environ.get("APP_ID", "b699d721-4f6f-4320-bc9a-88d578dfe68f") auth_id = os.environ.get("APP_AUTH_ID", "72f988bf-86f1-41af-91ab-2d7cd011db47") app_key = os.environ.get("APP_KEY") if app_id and app_key and auth_id: provider = ApplicationKeyTokenProvider(KUSTO_URI, auth_id, app_id, app_key) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert TokenProviderTests.get_token_value(token) is not None else: print(" *** Skipped App Id & Key Provider Test ***") @staticmethod def test_app_cert_provider(): # default details are for kusto-client-e2e-test-app # to run the test download the certs from Azure Portal cert_app_id = os.environ.get("CERT_APP_ID", "b699d721-4f6f-4320-bc9a-88d578dfe68f") cert_auth = os.environ.get("CERT_AUTH", "72f988bf-86f1-41af-91ab-2d7cd011db47") thumbprint = os.environ.get("CERT_THUMBPRINT") public_cert_path = os.environ.get("PUBLIC_CERT_PATH") pem_key_path = os.environ.get("CERT_PEM_KEY_PATH") if pem_key_path and thumbprint and cert_app_id: with open(pem_key_path, "rb") as file: pem_key = file.read() provider = ApplicationCertificateTokenProvider(KUSTO_URI, cert_app_id, cert_auth, pem_key, thumbprint) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert TokenProviderTests.get_token_value(token) is not None if public_cert_path: with open(public_cert_path, "r") as file: public_cert = file.read() provider = ApplicationCertificateTokenProvider(KUSTO_URI, cert_app_id, cert_auth, pem_key, thumbprint, public_cert) token = provider.get_token() assert TokenProviderTests.get_token_value(token) is not None # Again through cache token = provider._get_token_from_cache_impl() assert TokenProviderTests.get_token_value(token) is not None else: print(" *** Skipped App Cert SNI Provider Test ***") else: print(" *** Skipped App Cert Provider Test ***") @staticmethod def test_cloud_mfa_off(): FAKE_URI = "https://fake_cluster_for_login_mfa_test.kusto.windows.net" cloud = CloudInfo( login_endpoint="https://login_endpoint", login_mfa_required=False, kusto_client_app_id="1234", kusto_client_redirect_uri="", kusto_service_resource_id="https://fakeurl.kusto.windows.net", first_party_authority_url="", ) CloudSettings._cloud_cache[FAKE_URI] = cloud authority = "auth_test" provider = UserPassTokenProvider(FAKE_URI, authority, "a", "b") provider._init_once(init_only_resources=True) context = provider.context() assert context["authority"] == "https://login_endpoint/auth_test" assert context["client_id"] == cloud.kusto_client_app_id assert provider._scopes == ["https://fakeurl.kusto.windows.net/.default"] @staticmethod def test_cloud_mfa_on(): FAKE_URI = "https://fake_cluster_for_login_mfa_test.kusto.windows.net" cloud = CloudInfo( login_endpoint="https://login_endpoint", login_mfa_required=True, kusto_client_app_id="1234", kusto_client_redirect_uri="", kusto_service_resource_id="https://fakeurl.kusto.windows.net", first_party_authority_url="", ) CloudSettings._cloud_cache[FAKE_URI] = cloud authority = "auth_test" provider = UserPassTokenProvider(FAKE_URI, authority, "a", "b") provider._init_once(init_only_resources=True) context = provider.context() assert context["authority"] == "https://login_endpoint/auth_test" assert context["client_id"] == "1234" assert provider._scopes == ["https://fakeurl.kustomfa.windows.net/.default"] azure-kusto-python-3.0.1/azure-kusto-data/tox.ini000066400000000000000000000001331417222763000220370ustar00rootroot00000000000000[tox] envlist = py27,py35,py37 [testenv] deps=mock pytest pandas commands = pytestazure-kusto-python-3.0.1/azure-kusto-ingest/000077500000000000000000000000001417222763000211075ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-ingest/README.rst000066400000000000000000000036251417222763000226040ustar00rootroot00000000000000Microsoft Azure Kusto Ingest Library for Python =============================================== .. code-block:: python from azure.kusto.data import KustoConnectionStringBuilder, DataFormat from azure.kusto.ingest import QueuedIngestClient, IngestionProperties, FileDescriptor, BlobDescriptor ingestion_props = IngestionProperties(database="{database_name}", table="{table_name}", data_format=DataFormat.CSV) client = QueuedIngestClient(KustoConnectionStringBuilder.with_interactive_login("https://ingest-{cluster_name}.kusto.windows.net")) file_descriptor = FileDescriptor("{filename}.csv", 15360) # in this example, the raw (uncompressed) size of the data is 15KB (15360 bytes) client.ingest_from_file(file_descriptor, ingestion_properties=ingestion_props) client.ingest_from_file("{filename}.csv", ingestion_properties=ingestion_props) blob_descriptor = BlobDescriptor("https://{path_to_blob}.csv.gz?sas", 51200) # in this example, the raw (uncompressed) size of the data is 50KB (52100 bytes) client.ingest_from_blob(blob_descriptor, ingestion_properties=ingestion_props) Overview -------- *Kusto Python Ingest Client* Library provides the capability to ingest data into Kusto clusters using Python. It is Python 3.x compatible and supports data types through familiar Python DB API interface. It's possible to use the library, for instance, from `Jupyter Notebooks `_ which are attached to Spark clusters, including, but not exclusively, `Azure Databricks `_ instances. * `How to install the package `_. * `Data ingest sample `_. * `GitHub Repository `_. azure-kusto-python-3.0.1/azure-kusto-ingest/azure/000077500000000000000000000000001417222763000222355ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-ingest/azure/__init__.py000066400000000000000000000003611417222763000243460ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License """Init file for azure namespace. https://github.com/Azure/azure-sdk-for-python/wiki/Azure-packaging""" __path__ = __import__("pkgutil").extend_path(__path__, __name__) azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/000077500000000000000000000000001417222763000234025ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/__init__.py000066400000000000000000000003611417222763000255130ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License """Init file for azure namespace. https://github.com/Azure/azure-sdk-for-python/wiki/Azure-packaging""" __path__ = __import__("pkgutil").extend_path(__path__, __name__) azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/000077500000000000000000000000001417222763000246735ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/__init__.py000066400000000000000000000014141417222763000270040ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License from ._version import VERSION as __version__ from .base_ingest_client import IngestionResult, IngestionStatus from .descriptors import BlobDescriptor, FileDescriptor, StreamDescriptor from .exceptions import KustoMissingMappingError from .ingest_client import QueuedIngestClient from .ingestion_properties import ( ValidationPolicy, ValidationImplications, ValidationOptions, ReportLevel, ReportMethod, IngestionProperties, IngestionMappingKind, ColumnMapping, TransformationMethod, ) from .managed_streaming_ingest_client import ManagedStreamingIngestClient from .streaming_ingest_client import KustoStreamingIngestClient from .base_ingest_client import BaseIngestClient azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/_resource_manager.py000066400000000000000000000140111417222763000307220ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import re from datetime import datetime, timedelta from typing import List from azure.kusto.data import KustoClient from azure.kusto.data._models import KustoResultTable _URI_FORMAT = re.compile("https://(\\w+).(queue|blob|table).(core.\\w+.\\w+)/([\\w,-]+)\\?(.*)") _SHOW_VERSION = ".show version" _SERVICE_TYPE_COLUMN_NAME = "ServiceType" class _ResourceUri: def __init__(self, storage_account_name: str, object_type: str, endpoint_suffix: str, object_name: str, sas: str): self.storage_account_name = storage_account_name self.object_type = object_type self.endpoint_suffix = endpoint_suffix self.object_name = object_name self.sas = sas @classmethod def parse(cls, uri): """Parses uri into a ResourceUri object""" match = _URI_FORMAT.search(uri) return cls(match.group(1), match.group(2), match.group(3), match.group(4), match.group(5)) @property def uri(self) -> str: return "https://{0.storage_account_name}.{0.object_type}.{0.endpoint_suffix}/{0.object_name}".format(self) @property def account_uri(self) -> str: return "https://{0.storage_account_name}.{0.object_type}.{0.endpoint_suffix}/?{0.sas}".format(self) def __str__(self): return "https://{0.storage_account_name}.{0.object_type}.{0.endpoint_suffix}/{0.object_name}?{0.sas}" class _IngestClientResources: def __init__( self, secured_ready_for_aggregation_queues: List[_ResourceUri] = None, failed_ingestions_queues: List[_ResourceUri] = None, successful_ingestions_queues: List[_ResourceUri] = None, containers: List[_ResourceUri] = None, status_tables: List[_ResourceUri] = None, ): self.secured_ready_for_aggregation_queues = secured_ready_for_aggregation_queues self.failed_ingestions_queues = failed_ingestions_queues self.successful_ingestions_queues = successful_ingestions_queues self.containers = containers self.status_tables = status_tables def is_applicable(self): resources = [ self.secured_ready_for_aggregation_queues, self.failed_ingestions_queues, self.failed_ingestions_queues, self.containers, self.status_tables, ] return all(resources) class _ResourceManager: def __init__(self, kusto_client: KustoClient): self._kusto_client = kusto_client self._refresh_period = timedelta(hours=1) self._ingest_client_resources = None self._ingest_client_resources_last_update = None self._authorization_context = None self._authorization_context_last_update = None def _refresh_ingest_client_resources(self): if ( not self._ingest_client_resources or (self._ingest_client_resources_last_update + self._refresh_period) <= datetime.utcnow() or not self._ingest_client_resources.is_applicable() ): self._ingest_client_resources = self._get_ingest_client_resources_from_service() self._ingest_client_resources_last_update = datetime.utcnow() def _get_resource_by_name(self, table: KustoResultTable, resource_name: str): return [_ResourceUri.parse(row["StorageRoot"]) for row in table if row["ResourceTypeName"] == resource_name] def _get_ingest_client_resources_from_service(self): table = self._kusto_client.execute("NetDefaultDB", ".get ingestion resources").primary_results[0] secured_ready_for_aggregation_queues = self._get_resource_by_name(table, "SecuredReadyForAggregationQueue") failed_ingestions_queues = self._get_resource_by_name(table, "FailedIngestionsQueue") successful_ingestions_queues = self._get_resource_by_name(table, "SuccessfulIngestionsQueue") containers = self._get_resource_by_name(table, "TempStorage") status_tables = self._get_resource_by_name(table, "IngestionsStatusTable") return _IngestClientResources(secured_ready_for_aggregation_queues, failed_ingestions_queues, successful_ingestions_queues, containers, status_tables) def _refresh_authorization_context(self): if ( not self._authorization_context or self._authorization_context.isspace() or (self._authorization_context_last_update + self._refresh_period) <= datetime.utcnow() ): self._authorization_context = self._get_authorization_context_from_service() self._authorization_context_last_update = datetime.utcnow() def _get_authorization_context_from_service(self): return self._kusto_client.execute("NetDefaultDB", ".get kusto identity token").primary_results[0][0]["AuthorizationContext"] def get_ingestion_queues(self) -> List[_ResourceUri]: self._refresh_ingest_client_resources() return self._ingest_client_resources.secured_ready_for_aggregation_queues def get_failed_ingestions_queues(self) -> List[_ResourceUri]: self._refresh_ingest_client_resources() return self._ingest_client_resources.failed_ingestions_queues def get_successful_ingestions_queues(self) -> List[_ResourceUri]: self._refresh_ingest_client_resources() return self._ingest_client_resources.successful_ingestions_queues def get_containers(self) -> List[_ResourceUri]: self._refresh_ingest_client_resources() return self._ingest_client_resources.containers def get_ingestions_status_tables(self) -> List[_ResourceUri]: self._refresh_ingest_client_resources() return self._ingest_client_resources.status_tables def get_authorization_context(self): self._refresh_authorization_context() return self._authorization_context def retrieve_service_type(self): try: command_result = self._kusto_client.execute("NetDefaultDB", _SHOW_VERSION) return command_result.primary_results[0][0][_SERVICE_TYPE_COLUMN_NAME] except (TypeError, KeyError): return "" azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/_retry.py000066400000000000000000000011101417222763000265420ustar00rootroot00000000000000import random from time import sleep class ExponentialRetry: def __init__(self, max_attempts, sleep_base_secs: float = 1.0, max_jitter_secs: float = 1.0): self.max_attempts = max_attempts self.sleep_base_secs = sleep_base_secs self.max_jitter_secs = max_jitter_secs self.current_attempt = 0 def do_backoff(self): sleep((self.sleep_base_secs * (2 ** self.current_attempt)) + (random.uniform(0, self.max_jitter_secs))) self.current_attempt += 1 def __bool__(self): return self.current_attempt < self.max_attempts azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/_status_q.py000066400000000000000000000113371417222763000272540ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import random from typing import List, Callable from azure.kusto.ingest._resource_manager import _ResourceUri from azure.storage.queue import QueueServiceClient, QueueClient, QueueMessage, TextBase64EncodePolicy, TextBase64DecodePolicy class QueueDetails: def __init__(self, name, service): self.name = name self.service = service def __str__(self): return "QueueDetails({0.name})".format(self) class StatusQueue: """StatusQueue is a class to simplify access to Kusto status queues (backed by azure storage queues).""" def __init__(self, get_queues_func: Callable[[], List[_ResourceUri]], message_cls): self.get_queues_func = get_queues_func self.message_cls = message_cls def _get_queues(self) -> List[QueueClient]: return [ QueueServiceClient(q.account_uri).get_queue_client(queue=q.object_name, message_decode_policy=TextBase64DecodePolicy()) for q in self.get_queues_func() ] def is_empty(self) -> bool: """Checks if Status queue has any messages""" return len(self.peek(1, raw=True)) == 0 def _deserialize_message(self, m: QueueMessage): """Deserialize a message and return at as `message_cls` :param m: original message m. """ return self.message_cls(m.content) # TODO: current implementation takes a union top n / len(queues), which is not ideal, # because the user is not supposed to know that there can be multiple underlying queues def peek(self, n=1, raw=False) -> List[QueueMessage]: """Peek status queue :param int n: number of messages to return as part of peek. :param bool raw: should message content be returned as is (no parsing). """ def _peek_specific_q(_q: QueueClient, _n: int) -> bool: has_messages = False for m in _q.peek_messages(max_messages=_n): if m: has_messages = True result.append(m if raw else self._deserialize_message(m)) # short circuit to prevent unneeded work if len(result) == n: return True return has_messages queues = self._get_queues() random.shuffle(queues) per_q = int(n / len(queues)) + 1 result = [] non_empty_qs = [] for q in queues: if _peek_specific_q(q, per_q): non_empty_qs.append(q) if len(result) == n: return result # in-case queues aren't balanced, and we didn't get enough messages, iterate again and this time get all that we can for q in non_empty_qs: _peek_specific_q(q, n) if len(result) == n: return result # because we ask for n / len(qs) + 1, we might get more message then requests return result # TODO: current implementation takes a union top n / len(queues), which is not ideal, # because the user is not supposed to know that there can be multiple underlying queues def pop(self, n: int = 1, raw: bool = False, delete: bool = True) -> List[QueueMessage]: """Pop status queue :param int n: number of messages to return as part of peek. :param bool raw: should message content be returned as is (no parsing). :param bool delete: should message be deleted after pop. default is True as this is expected of a q. """ def _pop_specific_q(_q: QueueClient, _n: int) -> bool: has_messages = False for m in _q.receive_messages(messages_per_page=_n): if m: has_messages = True result.append(m if raw else self._deserialize_message(m)) if delete: _q.delete_message(m.id, m.pop_receipt) # short circuit to prevent unneeded work if len(result) == n: return True return has_messages queues = self._get_queues() random.shuffle(queues) per_q = int(n / len(queues)) + 1 result = [] non_empty_qs = [] for q in queues: if _pop_specific_q(q, per_q): non_empty_qs.append(q) if len(result) == n: return result # in-case queues aren't balanced, and we didn't get enough messages, iterate again and this time get all that we can for q in non_empty_qs: _pop_specific_q(q, n) if len(result) == n: return result # because we ask for n / len(qs) + 1, we might get more message then requests return result azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/_stream_extensions.py000066400000000000000000000045211417222763000311600ustar00rootroot00000000000000import io from typing import IO, AnyStr def read_until_size_or_end(stream: IO[AnyStr], size: int) -> io.BytesIO: pos = 0 result = io.BytesIO() while True: try: returned = stream.read(size - pos) pos += len(returned) result.write(returned) if len(returned) == 0 or pos == size: result.seek(0, io.SEEK_SET) return result except BlockingIOError: continue class ChainStream(io.RawIOBase): """ https://stackoverflow.com/questions/24528278/stream-multiple-files-into-a-readable-object-in-python """ def __init__(self, streams): self.leftover = b"" self.stream_iter = iter(streams) try: self.stream = next(self.stream_iter) except StopIteration: self.stream = None def readable(self): return True def _read_next_chunk(self, max_length): # Return 0 or more bytes from the current stream, first returning all # leftover bytes. If the stream is closed returns b'' if self.leftover: return self.leftover elif self.stream is not None: return self.stream.read(max_length) else: return b"" def readinto(self, b): buffer_length = len(b) chunk = self._read_next_chunk(buffer_length) while len(chunk) == 0: # move to next stream if self.stream is not None: self.stream.close() try: self.stream = next(self.stream_iter) chunk = self._read_next_chunk(buffer_length) except StopIteration: # No more streams to chain together self.stream = None return 0 # indicate EOF output, self.leftover = chunk[:buffer_length], chunk[buffer_length:] b[: len(output)] = output return len(output) def chain_streams(streams, buffer_size=io.DEFAULT_BUFFER_SIZE): """ Chain an iterable of streams together into a single buffered stream. Usage: def generate_open_file_streams(): for file in filenames: yield open(file, 'rb') f = chain_streams(generate_open_file_streams()) f.read() """ return io.BufferedReader(ChainStream(streams), buffer_size=buffer_size) azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/_version.py000066400000000000000000000001321417222763000270650ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License VERSION = "3.0.1" azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/base_ingest_client.py000066400000000000000000000114051417222763000310670ustar00rootroot00000000000000import os import tempfile import time import uuid from abc import ABCMeta, abstractmethod from copy import copy from enum import Enum from gzip import GzipFile from io import TextIOWrapper, BytesIO from typing import TYPE_CHECKING, Union, IO, AnyStr, Optional from azure.kusto.data.data_format import DataFormat from .descriptors import FileDescriptor, StreamDescriptor from .ingestion_properties import IngestionProperties if TYPE_CHECKING: import pandas class IngestionStatus(Enum): """ The ingestion was queued. """ QUEUED = "QUEUED" """ The ingestion was successfully streamed """ SUCCESS = "SUCCESS" class IngestionResult: """ The result of an ingestion. """ status: IngestionStatus "Will be `Queued` if the ingestion is queued, or `Success` if the ingestion is streaming and successful." database: str "The name of the database where the ingestion was performed." table: str "The name of the table where the ingestion was performed." source_id: uuid.UUID "The source id of the ingestion." blob_uri: Optional[str] "The blob uri of the ingestion, if exists." def __init__(self, status: IngestionStatus, database: str, table: str, source_id: uuid.UUID, blob_uri: Optional[str] = None): self.status = status self.database = database self.table = table self.source_id = source_id self.blob_uri = blob_uri def __repr__(self): return f"IngestionResult(status={self.status}, database={self.database}, table={self.table}, source_id={self.source_id}, blob_uri={self.blob_uri})" class BaseIngestClient(metaclass=ABCMeta): @abstractmethod def ingest_from_file(self, file_descriptor: Union[FileDescriptor, str], ingestion_properties: IngestionProperties) -> IngestionResult: """Ingest from local files. :param file_descriptor: a FileDescriptor to be ingested. :param azure.kusto.ingest.IngestionProperties ingestion_properties: Ingestion properties. """ pass @abstractmethod def ingest_from_stream(self, stream_descriptor: Union[StreamDescriptor, IO[AnyStr]], ingestion_properties: IngestionProperties) -> IngestionResult: """Ingest from io streams. :param stream_descriptor: An object that contains a description of the stream to be ingested. :param azure.kusto.ingest.IngestionProperties ingestion_properties: Ingestion properties. """ pass def ingest_from_dataframe(self, df: "pandas.DataFrame", ingestion_properties: IngestionProperties) -> IngestionResult: """Enqueue an ingest command from local files. To learn more about ingestion methods go to: https://docs.microsoft.com/en-us/azure/data-explorer/ingest-data-overview#ingestion-methods :param pandas.DataFrame df: input dataframe to ingest. :param azure.kusto.ingest.IngestionProperties ingestion_properties: Ingestion properties. """ from pandas import DataFrame if not isinstance(df, DataFrame): raise ValueError("Expected DataFrame instance, found {}".format(type(df))) file_name = "df_{id}_{timestamp}_{uid}.csv.gz".format(id=id(df), timestamp=int(time.time()), uid=uuid.uuid4()) temp_file_path = os.path.join(tempfile.gettempdir(), file_name) df.to_csv(temp_file_path, index=False, encoding="utf-8", header=False, compression="gzip") ingestion_properties.format = DataFormat.CSV try: return self.ingest_from_file(temp_file_path, ingestion_properties) finally: os.unlink(temp_file_path) @staticmethod def _prepare_stream(stream_descriptor: Union[StreamDescriptor, IO[AnyStr]], ingestion_properties: IngestionProperties) -> StreamDescriptor: if not isinstance(stream_descriptor, StreamDescriptor): new_descriptor = StreamDescriptor(stream_descriptor) else: new_descriptor = copy(stream_descriptor) if isinstance(new_descriptor.stream, TextIOWrapper): stream = new_descriptor.stream.buffer else: stream = new_descriptor.stream if not new_descriptor.is_compressed and ingestion_properties.format.compressible: zipped_stream = BytesIO() buffer = stream.read() with GzipFile(filename="data", fileobj=zipped_stream, mode="wb") as f_out: if isinstance(buffer, str): data = bytes(buffer, "utf-8") f_out.write(data) else: f_out.write(buffer) zipped_stream.seek(0) new_descriptor.is_compressed = True new_descriptor.stream_name += ".gz" stream = zipped_stream new_descriptor.stream = stream return new_descriptor azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/descriptors.py000066400000000000000000000134331417222763000276120ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import os import shutil import struct import uuid from gzip import GzipFile from io import BytesIO, SEEK_END from typing import Union, Optional, AnyStr, IO from zipfile import ZipFile OptionalUUID = Optional[Union[str, uuid.UUID]] def ensure_uuid(maybe_uuid: OptionalUUID) -> uuid.UUID: if not maybe_uuid: return uuid.uuid4() if type(maybe_uuid) == uuid.UUID: return maybe_uuid return uuid.UUID(f"{maybe_uuid}", version=4) class FileDescriptor: """FileDescriptor is used to describe a file that will be used as an ingestion source.""" # Gzip keeps the decompressed stream size as a UINT32 in the last 4 bytes of the stream, however this poses a limit to the expressed size which is 4GB # The standard says that when the size is bigger then 4GB, the UINT rolls over. # The below constant expresses the maximal size of a compressed stream that will not cause the UINT32 to rollover given a maximal compression ratio of 1:40 GZIP_MAX_DISK_SIZE_FOR_DETECTION = int(4 * 1024 * 1024 * 1024 / 40) DEFAULT_COMPRESSION_RATIO = 11 def __init__(self, path: str, size: Optional[int] = None, source_id: OptionalUUID = None): """ :param path: file path. :type path: str. :param size: estimated size of file if known. if None or 0 will try to guess. :type size: Optional[int]. :param source_id: a v4 uuid to serve as the source's id. :type source_id: OptionalUUID """ self.path: str = path self._size: Optional[int] = size self._detect_size_once: bool = not size self.source_id: uuid.UUID = ensure_uuid(source_id) self.stream_name: str = os.path.basename(self.path) @property def size(self) -> int: if self._detect_size_once: self._detect_size() self._detect_size_once = False return self._size @size.setter def size(self, size: int): if size: self._size = size self._detect_size_once = False def _detect_size(self): uncompressed_size = 0 if self.path.endswith(".gz"): # This logic follow after the C# implementation # See IngstionHelpers.cs for an explanation as to what stands behind it with open(self.path, "rb") as f: disk_size = f.seek(-4, SEEK_END) uncompressed_size = struct.unpack("I", f.read(4))[0] if (disk_size >= uncompressed_size) or (disk_size >= self.GZIP_MAX_DISK_SIZE_FOR_DETECTION): uncompressed_size = disk_size * self.DEFAULT_COMPRESSION_RATIO elif self.path.endswith(".zip"): with ZipFile(self.path) as zip_archive: for f in zip_archive.infolist(): uncompressed_size += f.file_size else: uncompressed_size = os.path.getsize(self.path) self._size = uncompressed_size @property def is_compressed(self) -> bool: return self.path.endswith(".gz") or self.path.endswith(".zip") def open(self, should_compress: bool) -> BytesIO: if should_compress: self.stream_name += ".gz" file_stream = BytesIO() with open(self.path, "rb") as f_in, GzipFile(filename="data", fileobj=file_stream, mode="wb") as f_out: shutil.copyfileobj(f_in, f_out) file_stream.seek(0) else: file_stream = open(self.path, "rb") return file_stream class BlobDescriptor: """FileDescriptor is used to describe a file that will be used as an ingestion source""" def __init__(self, path: str, size: Optional[int] = None, source_id: OptionalUUID = None): """ :param path: blob uri. :type path: str. :param size: estimated size of file if known. :type size: Optional[int]. :param source_id: a v4 uuid to serve as the sources id. :type source_id: OptionalUUID """ self.path: str = path self.size: Optional[int] = size self.source_id: uuid.UUID = ensure_uuid(source_id) class StreamDescriptor: """StreamDescriptor is used to describe a stream that will be used as ingestion source""" # TODO: currently we always assume that streams are gz compressed (will get compressed before sending), should we expand that? def __init__( self, stream: IO[AnyStr], source_id: OptionalUUID = None, is_compressed: bool = False, stream_name: Optional[str] = None, size: Optional[int] = None ): """ :param stream: in-memory stream object. :type stream: io.BaseIO :param source_id: a v4 uuid to serve as the sources id. :type source_id: OptionalUUID :param is_compressed: specify if the provided stream is compressed :type is_compressed: boolean """ self.stream: IO[AnyStr] = stream self.source_id: uuid.UUID = ensure_uuid(source_id) self.is_compressed: bool = is_compressed self.stream_name: str = stream_name if self.stream_name is None: self.stream_name = "stream" if is_compressed: self.stream_name += ".gz" self.size: Optional[int] = size @staticmethod def from_file_descriptor(file_descriptor: Union[FileDescriptor, str]) -> "StreamDescriptor": if isinstance(file_descriptor, FileDescriptor): descriptor = file_descriptor else: descriptor = FileDescriptor(file_descriptor) stream = open(descriptor.path, "rb") is_compressed = descriptor.path.endswith(".gz") or descriptor.path.endswith(".zip") stream_descriptor = StreamDescriptor(stream, descriptor.source_id, is_compressed, descriptor.stream_name, descriptor.size) return stream_descriptor azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/exceptions.py000066400000000000000000000027171417222763000274350ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License from azure.kusto.data.data_format import DataFormat, IngestionMappingKind from azure.kusto.data.exceptions import KustoClientError class KustoMappingError(KustoClientError): """ Raised when the provided mapping arguments are invalid. """ class KustoDuplicateMappingError(KustoClientError): """ Raised when ingestion properties include both column mappings and a mapping reference """ def __init__(self): message = "Ingestion properties can't contain both an explicit mapping and a mapping reference." super(KustoDuplicateMappingError, self).__init__(message) class KustoMissingMappingError(KustoClientError): """ Raised when ingestion properties has data format that requires a mapping and is not defined, or when provided a mapping kind without a mapping. """ class KustoInvalidEndpointError(KustoClientError): """Raised when trying to ingest to invalid cluster type.""" def __init__(self, expected_service_type, actual_service_type, suggested_endpoint_url=None): message = f"You are using '{expected_service_type}' client type, but the provided endpoint is of ServiceType '{actual_service_type}'. Initialize the client with the appropriate endpoint URI" if suggested_endpoint_url: message = message + ": '" + suggested_endpoint_url + "'" super(KustoInvalidEndpointError, self).__init__(message) azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/ingest_client.py000066400000000000000000000205321417222763000300760ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import random import uuid from typing import Union, AnyStr, IO, List, Optional from urllib.parse import urlparse from azure.storage.blob import BlobServiceClient from azure.storage.queue import QueueServiceClient, TextBase64EncodePolicy from azure.kusto.data import KustoClient, KustoConnectionStringBuilder from azure.kusto.data.exceptions import KustoServiceError, KustoBlobError from .ingestion_blob_info import IngestionBlobInfo from ._resource_manager import _ResourceManager, _ResourceUri from .base_ingest_client import BaseIngestClient, IngestionResult, IngestionStatus from .descriptors import BlobDescriptor, FileDescriptor, StreamDescriptor from .exceptions import KustoInvalidEndpointError from .ingestion_properties import IngestionProperties class QueuedIngestClient(BaseIngestClient): """ Queued ingest client provides methods to allow queued ingestion into kusto (ADX). To learn more about the different types of ingestions and when to use each, visit: https://docs.microsoft.com/en-us/azure/data-explorer/ingest-data-overview#ingestion-methods """ _INGEST_PREFIX = "ingest-" _EXPECTED_SERVICE_TYPE = "DataManagement" def __init__(self, kcsb: Union[str, KustoConnectionStringBuilder]): """Kusto Ingest Client constructor. :param kcsb: The connection string to initialize KustoClient. """ if not isinstance(kcsb, KustoConnectionStringBuilder): kcsb = KustoConnectionStringBuilder(kcsb) self._connection_datasource = kcsb.data_source self._resource_manager = _ResourceManager(KustoClient(kcsb)) self._endpoint_service_type = None self._suggested_endpoint_uri = None def ingest_from_file(self, file_descriptor: Union[FileDescriptor, str], ingestion_properties: IngestionProperties) -> IngestionResult: """Enqueue an ingest command from local files. To learn more about ingestion methods go to: https://docs.microsoft.com/en-us/azure/data-explorer/ingest-data-overview#ingestion-methods :param file_descriptor: a FileDescriptor to be ingested. :param azure.kusto.ingest.IngestionProperties ingestion_properties: Ingestion properties. """ containers = self._get_containers() if isinstance(file_descriptor, FileDescriptor): descriptor = file_descriptor else: descriptor = FileDescriptor(file_descriptor) should_compress = not descriptor.is_compressed and ingestion_properties.format.compressible with descriptor.open(should_compress) as stream: blob_descriptor = QueuedIngestClient._upload_blob(containers, descriptor, ingestion_properties, stream) result = self.ingest_from_blob(blob_descriptor, ingestion_properties=ingestion_properties) return result def ingest_from_stream(self, stream_descriptor: Union[StreamDescriptor, IO[AnyStr]], ingestion_properties: IngestionProperties) -> IngestionResult: """Ingest from io streams. :param stream_descriptor: An object that contains a description of the stream to be ingested. :param azure.kusto.ingest.IngestionProperties ingestion_properties: Ingestion properties. """ containers = self._get_containers() stream_descriptor = BaseIngestClient._prepare_stream(stream_descriptor, ingestion_properties) blob_descriptor = QueuedIngestClient._upload_blob(containers, stream_descriptor, ingestion_properties, stream_descriptor.stream) return self.ingest_from_blob(blob_descriptor, ingestion_properties=ingestion_properties) def ingest_from_blob(self, blob_descriptor: BlobDescriptor, ingestion_properties: IngestionProperties) -> IngestionResult: """Enqueue an ingest command from azure blobs. To learn more about ingestion methods go to: https://docs.microsoft.com/en-us/azure/data-explorer/ingest-data-overview#ingestion-methods :param azure.kusto.ingest.BlobDescriptor blob_descriptor: An object that contains a description of the blob to be ingested. :param azure.kusto.ingest.IngestionProperties ingestion_properties: Ingestion properties. """ try: queues = self._resource_manager.get_ingestion_queues() except KustoServiceError as ex: self._validate_endpoint_service_type() raise ex random_queue = random.choice(queues) queue_service = QueueServiceClient(random_queue.account_uri) authorization_context = self._resource_manager.get_authorization_context() ingestion_blob_info = IngestionBlobInfo(blob_descriptor, ingestion_properties=ingestion_properties, auth_context=authorization_context) ingestion_blob_info_json = ingestion_blob_info.to_json() queue_client = queue_service.get_queue_client(queue=random_queue.object_name, message_encode_policy=TextBase64EncodePolicy()) queue_client.send_message(content=ingestion_blob_info_json) return IngestionResult( IngestionStatus.QUEUED, ingestion_properties.database, ingestion_properties.table, blob_descriptor.source_id, blob_descriptor.path ) def _get_containers(self) -> List[_ResourceUri]: try: containers = self._resource_manager.get_containers() except KustoServiceError as ex: self._validate_endpoint_service_type() raise ex return containers @staticmethod def _upload_blob( containers: List[_ResourceUri], descriptor: Union[FileDescriptor, StreamDescriptor], ingestion_properties: IngestionProperties, stream: IO[AnyStr], ) -> BlobDescriptor: blob_name = "{db}__{table}__{guid}__{file}".format( db=ingestion_properties.database, table=ingestion_properties.table, guid=descriptor.source_id, file=descriptor.stream_name ) random_container = random.choice(containers) try: blob_service = BlobServiceClient(random_container.account_uri) blob_client = blob_service.get_blob_client(container=random_container.object_name, blob=blob_name) blob_client.upload_blob(data=stream) except Exception as e: raise KustoBlobError(e) return BlobDescriptor(blob_client.url, descriptor.size, descriptor.source_id) def _validate_endpoint_service_type(self): if not self._hostname_starts_with_ingest(self._connection_datasource): if not self._endpoint_service_type: self._endpoint_service_type = self._retrieve_service_type() if self._EXPECTED_SERVICE_TYPE != self._endpoint_service_type: if not self._suggested_endpoint_uri: self._suggested_endpoint_uri = self._generate_endpoint_suggestion(self._connection_datasource) if not self._suggested_endpoint_uri: raise KustoInvalidEndpointError(self._EXPECTED_SERVICE_TYPE, self._endpoint_service_type) raise KustoInvalidEndpointError(self._EXPECTED_SERVICE_TYPE, self._endpoint_service_type, self._suggested_endpoint_uri) def _retrieve_service_type(self) -> str: return self._resource_manager.retrieve_service_type() def _generate_endpoint_suggestion(self, datasource: str) -> Optional[str]: """The default is not passing a suggestion to the exception String""" endpoint_uri_to_suggest_str = None if datasource.strip(): try: endpoint_uri_to_suggest = urlparse(datasource) # Standardize URL formatting endpoint_uri_to_suggest = urlparse(endpoint_uri_to_suggest.scheme + "://" + self._INGEST_PREFIX + endpoint_uri_to_suggest.hostname) endpoint_uri_to_suggest_str = endpoint_uri_to_suggest.geturl() except Exception: # TODO: Add logging infrastructure so we can tell the user as a warning: # "Couldn't generate suggested endpoint due to problem parsing datasource, with exception: {ex}. The correct endpoint is usually the Engine endpoint with '{self._INGEST_PREFIX}' prepended to the hostname." pass return endpoint_uri_to_suggest_str def _hostname_starts_with_ingest(self, datasource: str) -> bool: datasource_uri = urlparse(datasource) hostname = datasource_uri.hostname return hostname and hostname.startswith(self._INGEST_PREFIX) azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/ingestion_blob_info.py000066400000000000000000000067271417222763000312710ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import json import uuid from datetime import datetime from typing import TYPE_CHECKING if TYPE_CHECKING: from azure.kusto.ingest import BlobDescriptor, IngestionProperties class IngestionBlobInfo: def __init__(self, blob_descriptor: "BlobDescriptor", ingestion_properties: "IngestionProperties", auth_context=None): self.properties = dict() self.properties["BlobPath"] = blob_descriptor.path if blob_descriptor.size: self.properties["RawDataSize"] = blob_descriptor.size self.properties["DatabaseName"] = ingestion_properties.database self.properties["TableName"] = ingestion_properties.table self.properties["RetainBlobOnSuccess"] = True self.properties["FlushImmediately"] = ingestion_properties.flush_immediately self.properties["IgnoreSizeLimit"] = False self.properties["ReportLevel"] = ingestion_properties.report_level.value self.properties["ReportMethod"] = ingestion_properties.report_method.value self.properties["SourceMessageCreationTime"] = datetime.utcnow().isoformat() self.properties["Id"] = str(blob_descriptor.source_id) additional_properties = ingestion_properties.additional_properties or {} additional_properties["authorizationContext"] = auth_context tags = [] if ingestion_properties.additional_tags: tags.extend(ingestion_properties.additional_tags) if ingestion_properties.drop_by_tags: tags.extend(["drop-by:" + drop for drop in ingestion_properties.drop_by_tags]) if ingestion_properties.ingest_by_tags: tags.extend(["ingest-by:" + ingest for ingest in ingestion_properties.ingest_by_tags]) if tags: additional_properties["tags"] = _convert_list_to_json(tags) if ingestion_properties.ingest_if_not_exists: additional_properties["ingestIfNotExists"] = _convert_list_to_json(ingestion_properties.ingest_if_not_exists) if ingestion_properties.ingestion_mapping: json_string = _convert_dict_to_json(ingestion_properties.ingestion_mapping) additional_properties["ingestionMapping"] = json_string if ingestion_properties.ingestion_mapping_reference: additional_properties["ingestionMappingReference"] = ingestion_properties.ingestion_mapping_reference if ingestion_properties.ingestion_mapping_type: additional_properties["ingestionMappingType"] = ingestion_properties.ingestion_mapping_type.value if ingestion_properties.validation_policy: additional_properties["ValidationPolicy"] = _convert_dict_to_json(ingestion_properties.validation_policy) if ingestion_properties.format: additional_properties["format"] = ingestion_properties.format.kusto_value if additional_properties: self.properties["AdditionalProperties"] = additional_properties def to_json(self): """Converts this object to a json string""" return _convert_list_to_json(self.properties) def _convert_list_to_json(array): """Converts array to a json string""" return json.dumps(array, skipkeys=False, allow_nan=False, indent=None, separators=(",", ":")) def _convert_dict_to_json(array): """Converts array to a json string""" return json.dumps(array, skipkeys=False, allow_nan=False, indent=None, separators=(",", ":"), sort_keys=True, default=lambda o: o.__dict__) azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/ingestion_properties.py000066400000000000000000000167571417222763000315400ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License from enum import Enum, IntEnum from typing import List, Optional from azure.kusto.data.data_format import DataFormat, IngestionMappingKind from .exceptions import KustoDuplicateMappingError, KustoMissingMappingError, KustoMappingError class ValidationOptions(IntEnum): """Validation options to ingest command.""" DoNotValidate = 0 ValidateCsvInputConstantColumns = 1 ValidateCsvInputColumnLevelOnly = 2 class ValidationImplications(IntEnum): """Validation implications to ingest command.""" Fail = 0 BestEffort = 1 class ValidationPolicy: """Validation policy to ingest command.""" def __init__(self, validation_options=ValidationOptions.DoNotValidate, validation_implications=ValidationImplications.BestEffort): self.ValidationOptions = validation_options self.ValidationImplications = validation_implications class ReportLevel(IntEnum): """Report level to ingest command.""" FailuresOnly = 0 DoNotReport = 1 FailuresAndSuccesses = 2 class ReportMethod(IntEnum): """Report method to ingest command.""" Queue = 0 class TransformationMethod(Enum): """Transformations to configure over json column mapping To read more about mapping transformations look here: https://docs.microsoft.com/en-us/azure/kusto/management/mappings#mapping-transformations""" NONE = "None" PROPERTY_BAG_ARRAY_TO_DICTIONARY = ("PropertyBagArrayToDictionary",) SOURCE_LOCATION = "SourceLocation" SOURCE_LINE_NUMBER = "SourceLineNumber" GET_PATH_ELEMENT = "GetPathElement" UNKNOWN_ERROR = "UnknownMethod" DATE_TIME_FROM_UNIX_SECONDS = "DateTimeFromUnixSeconds" DATE_TIME_FROM_UNIX_MILLISECONDS = "DateTimeFromUnixMilliseconds" DATE_TIME_FROM_UNIX_MICROSECONDS = "DateTimeFromUnixMicroseconds" DATE_TIME_FROM_UNIX_NANOSECONDS = "DateTimeFromUnixNanoseconds" class ColumnMapping: """Use this class to create mappings for IngestionProperties.ingestionMappings and utilize mappings that were not pre-created (it is recommended to create the mappings in advance and use ingestionMappingReference). To read more about mappings look here: https://docs.microsoft.com/en-us/azure/kusto/management/mappings""" # Json Mapping consts PATH = "Path" TRANSFORMATION_METHOD = "Transform" # csv Mapping consts ORDINAL = "Ordinal" CONST_VALUE = "ConstValue" # Avro Mapping consts FIELD_NAME = "Field" COLUMNS = "Columns" # General Mapping consts STORAGE_DATA_TYPE = "StorageDataType" def __init__( self, column_name: str, column_type: str, path: str = None, transform: TransformationMethod = TransformationMethod.NONE, ordinal: int = None, const_value: str = None, field=None, columns=None, storage_data_type=None, ): self.column = column_name self.datatype = column_type self.properties = {} if path: self.properties[self.PATH] = path if transform != TransformationMethod.NONE: self.properties[self.TRANSFORMATION_METHOD] = transform.value if ordinal is not None: self.properties[self.ORDINAL] = str(ordinal) if const_value: self.properties[self.CONST_VALUE] = const_value if field: self.properties[self.FIELD_NAME] = field if columns: self.properties[self.COLUMNS] = columns if storage_data_type: self.properties[self.STORAGE_DATA_TYPE] = storage_data_type def is_valid(self, kind: IngestionMappingKind) -> bool: if not self.column: return False if kind in (IngestionMappingKind.JSON, IngestionMappingKind.PARQUET, IngestionMappingKind.ORC, IngestionMappingKind.W3CLOGFILE): return ( bool(self.properties.get(self.PATH)) or self.properties.get(self.TRANSFORMATION_METHOD) == TransformationMethod.SOURCE_LINE_NUMBER.value or self.properties.get(self.TRANSFORMATION_METHOD) == TransformationMethod.SOURCE_LOCATION.value ) if kind in (IngestionMappingKind.AVRO, IngestionMappingKind.APACHEAVRO): return bool(self.properties.get(self.COLUMNS)) return True class IngestionProperties: """ Class to represent ingestion properties. For more information check out https://docs.microsoft.com/en-us/azure/data-explorer/ingestion-properties """ def __init__( self, database: str, table: str, data_format: DataFormat = DataFormat.CSV, column_mappings: Optional[List[ColumnMapping]] = None, ingestion_mapping_kind: Optional[IngestionMappingKind] = None, ingestion_mapping_reference: Optional[str] = None, ingest_if_not_exists: Optional[List[str]] = None, ingest_by_tags: Optional[List[str]] = None, drop_by_tags: Optional[List[str]] = None, additional_tags: Optional[List[str]] = None, flush_immediately: bool = False, report_level: ReportLevel = ReportLevel.DoNotReport, report_method: ReportMethod = ReportMethod.Queue, validation_policy: Optional[ValidationPolicy] = None, additional_properties: Optional[dict] = None, ): if ingestion_mapping_reference is None and column_mappings is None: if data_format._mapping_required: raise KustoMissingMappingError(f"When stream format is '{data_format.kusto_value}', a mapping must be provided.") if ingestion_mapping_kind is not None: raise KustoMissingMappingError(f"When ingestion mapping kind is set ('{ingestion_mapping_kind.value}'), a mapping must be provided.") else: # A mapping is provided if ingestion_mapping_kind is not None: if data_format.ingestion_mapping_kind != ingestion_mapping_kind: raise KustoMappingError( f"Wrong ingestion mapping for format '{data_format.kusto_value}'; mapping kind should be '{data_format.ingestion_mapping_kind.value}', " f"but was '{ingestion_mapping_kind.value}'. " ) else: ingestion_mapping_kind = data_format.ingestion_mapping_kind if column_mappings is not None: if ingestion_mapping_reference is not None: raise KustoDuplicateMappingError() validation_errors = [ f"Column mapping '{mapping.column}' is invalid." for mapping in column_mappings if not mapping.is_valid(ingestion_mapping_kind) ] if validation_errors: errors = "\n".join(validation_errors) raise KustoMappingError(f"Failed with validation errors:\n{errors}") self.database = database self.table = table self.format = data_format self.ingestion_mapping = column_mappings self.ingestion_mapping_type = ingestion_mapping_kind self.ingestion_mapping_reference = ingestion_mapping_reference self.additional_tags = additional_tags self.ingest_if_not_exists = ingest_if_not_exists self.ingest_by_tags = ingest_by_tags self.drop_by_tags = drop_by_tags self.flush_immediately = flush_immediately self.report_level = report_level self.report_method = report_method self.validation_policy = validation_policy self.additional_properties = additional_properties azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/managed_streaming_ingest_client.py000066400000000000000000000136421417222763000336270ustar00rootroot00000000000000import uuid from io import SEEK_SET from typing import TYPE_CHECKING, Union, IO, AnyStr from azure.kusto.data import KustoConnectionStringBuilder from azure.kusto.data.exceptions import KustoApiError from . import IngestionProperties, BlobDescriptor, StreamDescriptor, FileDescriptor from ._retry import ExponentialRetry from ._stream_extensions import read_until_size_or_end, chain_streams from .base_ingest_client import BaseIngestClient, IngestionResult from .ingest_client import QueuedIngestClient from .streaming_ingest_client import KustoStreamingIngestClient if TYPE_CHECKING: pass class ManagedStreamingIngestClient(BaseIngestClient): """ Managed Streaming Ingestion Client. Will try to ingest with streaming, but if it fails, will fall back to queued ingestion. Each transient failure will be retried with exponential backoff. Managed streaming ingest client will fall back to queued if: - Multiple transient errors were encountered when trying to do streaming ingestion - The ingestion is too large for streaming ingestion (over 4MB) - The ingestion is directly for a blob """ MAX_STREAMING_SIZE_IN_BYTES = 4 * 1024 * 1024 ATTEMPT_COUNT = 3 @staticmethod def from_engine_kcsb(engine_kcsb: Union[KustoConnectionStringBuilder, str]) -> "ManagedStreamingIngestClient": """ Create a ManagedStreamingIngestClient from a KustoConnectionStringBuilder for the engine. This Connection String is used for the streaming ingest client. This method will infer the dm connection string (by using the same authentication and adding the ingest- prefix) For advanced use cases, use the constructor directly. :param engine_kcsb: KustoConnectionStringBuilder for the engine. :return: ManagedStreamingIngestClient """ kcsb = repr(engine_kcsb) if type(engine_kcsb) == KustoConnectionStringBuilder else engine_kcsb dm_kcsb = KustoConnectionStringBuilder(kcsb.replace("https://", "https://ingest-")) return ManagedStreamingIngestClient(engine_kcsb, dm_kcsb) @staticmethod def from_dm_kcsb(dm_kcsb: Union[KustoConnectionStringBuilder, str]) -> "ManagedStreamingIngestClient": """ Create a ManagedStreamingIngestClient from a KustoConnectionStringBuilder for the dm. This Connection String is used for the queued ingest client. This method will infer the engine connection string (by using the same authentication and removing the ingest- prefix) For advanced use cases, use the constructor directly. :param dm_kcsb: KustoConnectionStringBuilder for the dm. :return: ManagedStreamingIngestClient """ kcsb = repr(dm_kcsb) if type(dm_kcsb) == KustoConnectionStringBuilder else dm_kcsb engine_kcsb = KustoConnectionStringBuilder(kcsb.replace("https://ingest-", "https://")) return ManagedStreamingIngestClient(engine_kcsb, dm_kcsb) def __init__(self, engine_kcsb: Union[KustoConnectionStringBuilder, str], dm_kcsb: Union[KustoConnectionStringBuilder, str]): self.queued_client = QueuedIngestClient(dm_kcsb) self.streaming_client = KustoStreamingIngestClient(engine_kcsb) def ingest_from_file(self, file_descriptor: Union[FileDescriptor, str], ingestion_properties: IngestionProperties) -> IngestionResult: stream_descriptor = StreamDescriptor.from_file_descriptor(file_descriptor) with stream_descriptor.stream: return self.ingest_from_stream(stream_descriptor, ingestion_properties) def ingest_from_stream(self, stream_descriptor: Union[StreamDescriptor, IO[AnyStr]], ingestion_properties: IngestionProperties) -> IngestionResult: stream_descriptor = BaseIngestClient._prepare_stream(stream_descriptor, ingestion_properties) stream = stream_descriptor.stream buffered_stream = read_until_size_or_end(stream, self.MAX_STREAMING_SIZE_IN_BYTES + 1) if len(buffered_stream.getbuffer()) > self.MAX_STREAMING_SIZE_IN_BYTES: stream_descriptor.stream = chain_streams([buffered_stream, stream]) return self.queued_client.ingest_from_stream(stream_descriptor, ingestion_properties) stream_descriptor.stream = buffered_stream retry = self._create_exponential_retry() while retry: try: client_request_id = ManagedStreamingIngestClient._get_request_id(stream_descriptor.source_id, retry.current_attempt) return self.streaming_client._ingest_from_stream_with_client_request_id(stream_descriptor, ingestion_properties, client_request_id) except KustoApiError as e: error = e.get_api_error() if error.permanent: raise stream.seek(0, SEEK_SET) retry.do_backoff() return self.queued_client.ingest_from_stream(stream_descriptor, ingestion_properties) def ingest_from_blob(self, blob_descriptor: BlobDescriptor, ingestion_properties: IngestionProperties): """ Enqueue an ingest command from azure blobs. For ManagedStreamingIngestClient, this method always uses Queued Ingest, since it would be easier and faster to ingest blobs. To learn more about ingestion methods go to: https://docs.microsoft.com/en-us/azure/data-explorer/ingest-data-overview#ingestion-methods :param azure.kusto.ingest.BlobDescriptor blob_descriptor: An object that contains a description of the blob to be ingested. :param azure.kusto.ingest.IngestionProperties ingestion_properties: Ingestion properties. """ return self.queued_client.ingest_from_blob(blob_descriptor, ingestion_properties) @staticmethod def _get_request_id(source_id: uuid.UUID, attempt: int): return f"KPC.executeManagedStreamingIngest;{source_id};{attempt}" @staticmethod def _create_exponential_retry(): return ExponentialRetry(ManagedStreamingIngestClient.ATTEMPT_COUNT) azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/status.py000066400000000000000000000030221417222763000265650ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import json from ._status_q import StatusQueue class StatusMessage: OperationId = None Database = None Table = None IngestionSourceId = None IngestionSourcePath = None RootActivityId = None _raw = None def __init__(self, s): self._raw = s o = json.loads(s) for key, value in o.items(): if hasattr(self, key): try: setattr(self, key, value) except: # TODO: should we set up a logger? pass def __str__(self): return "{}".format(self._raw) def __repr__(self): return "{0.__class__.__name__}({0._raw})".format(self) class SuccessMessage(StatusMessage): SucceededOn = None class FailureMessage(StatusMessage): FailedOn = None Details = None ErrorCode = None FailureStatus = None OriginatesFromUpdatePolicy = None ShouldRetry = None class KustoIngestStatusQueues: """Kusto ingest Status Queue. Use this class to get status messages from Kusto status queues. Currently there are two queues exposed: `failure` and `success` queues. """ def __init__(self, kusto_ingest_client): self.success = StatusQueue(kusto_ingest_client._resource_manager.get_successful_ingestions_queues, message_cls=SuccessMessage) self.failure = StatusQueue(kusto_ingest_client._resource_manager.get_failed_ingestions_queues, message_cls=FailureMessage) azure-kusto-python-3.0.1/azure-kusto-ingest/azure/kusto/ingest/streaming_ingest_client.py000066400000000000000000000060451417222763000321520ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License from typing import Union, AnyStr, Optional from typing import IO from azure.kusto.data import KustoClient, KustoConnectionStringBuilder, ClientRequestProperties from .base_ingest_client import BaseIngestClient, IngestionResult, IngestionStatus from .descriptors import FileDescriptor, StreamDescriptor from .ingestion_properties import IngestionProperties class KustoStreamingIngestClient(BaseIngestClient): """Kusto streaming ingest client for Python. KustoStreamingIngestClient works with both 2.x and 3.x flavors of Python. All primitive types are supported. Tests are run using pytest. """ def __init__(self, kcsb: Union[KustoConnectionStringBuilder, str]): """Kusto Streaming Ingest Client constructor. :param KustoConnectionStringBuilder kcsb: The connection string to initialize KustoClient. """ self._kusto_client = KustoClient(kcsb) def ingest_from_file(self, file_descriptor: Union[FileDescriptor, str], ingestion_properties: IngestionProperties) -> IngestionResult: """Ingest from local files. :param file_descriptor: a FileDescriptor to be ingested. :param azure.kusto.ingest.IngestionProperties ingestion_properties: Ingestion properties. """ stream_descriptor = StreamDescriptor.from_file_descriptor(file_descriptor) with stream_descriptor.stream: return self.ingest_from_stream(stream_descriptor, ingestion_properties) def ingest_from_stream(self, stream_descriptor: Union[StreamDescriptor, IO[AnyStr]], ingestion_properties: IngestionProperties) -> IngestionResult: """Ingest from io streams. :param azure.kusto.ingest.StreamDescriptor stream_descriptor: An object that contains a description of the stream to be ingested. :param azure.kusto.ingest.IngestionProperties ingestion_properties: Ingestion properties. """ return self._ingest_from_stream_with_client_request_id(stream_descriptor, ingestion_properties, None) def _ingest_from_stream_with_client_request_id( self, stream_descriptor: Union[StreamDescriptor, IO[AnyStr]], ingestion_properties: IngestionProperties, client_request_id: Optional[str] ) -> IngestionResult: stream_descriptor = BaseIngestClient._prepare_stream(stream_descriptor, ingestion_properties) additional_properties = None if client_request_id: additional_properties = ClientRequestProperties() additional_properties.client_request_id = client_request_id self._kusto_client.execute_streaming_ingest( ingestion_properties.database, ingestion_properties.table, stream_descriptor.stream, ingestion_properties.format.name, additional_properties, mapping_name=ingestion_properties.ingestion_mapping_reference, ) return IngestionResult(IngestionStatus.SUCCESS, ingestion_properties.database, ingestion_properties.table, stream_descriptor.source_id) azure-kusto-python-3.0.1/azure-kusto-ingest/setup.cfg000066400000000000000000000002271417222763000227310ustar00rootroot00000000000000[bdist_wheel] universal=1 [flake8] ignore = E226,E302,E41 max-line-length = 160 exclude = tests/* max-complexity = 10 [pylint] max-line-length = 160 azure-kusto-python-3.0.1/azure-kusto-ingest/setup.py000066400000000000000000000030331417222763000226200ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import re from os import path from setuptools import setup, find_packages PACKAGE_NAME = "azure-kusto-ingest" # a-b-c => a/b/c PACKAGE_FOLDER_PATH = PACKAGE_NAME.replace("-", path.sep) # a-b-c => a.b.c NAMESPACE_NAME = PACKAGE_NAME.replace("-", ".") with open(path.join(PACKAGE_FOLDER_PATH, "_version.py"), "r") as fd: VERSION = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) if not VERSION: raise RuntimeError("Cannot find version information") setup( name=PACKAGE_NAME, version=VERSION, description="Kusto Ingest Client", long_description=open("README.rst", "r").read(), license="MIT", author="Microsoft Corporation", author_email="kustalk@microsoft.com", url="https://github.com/Azure/azure-kusto-python", namespace_packages=["azure"], classifiers=[ # 5 - Production/Stable depends on multi-threading / aio / perf "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "License :: OSI Approved :: MIT License", ], packages=find_packages(exclude=["azure", "tests"]), install_requires=["azure-kusto-data=={}".format(VERSION), "azure-storage-blob>=12,<13", "azure-storage-queue>=12,<13"], extras_require={"pandas": ["pandas"], "aio": []}, ) azure-kusto-python-3.0.1/azure-kusto-ingest/tests/000077500000000000000000000000001417222763000222515ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-ingest/tests/e2e.py000066400000000000000000000714331417222763000233060ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import io import json import os import pathlib import random import sys import time import unittest import uuid from datetime import datetime from typing import Optional import pytest from azure.kusto.data import KustoClient, KustoConnectionStringBuilder from azure.kusto.data._cloud_settings import CloudSettings from azure.kusto.data._models import WellKnownDataSet from azure.kusto.data.aio import KustoClient as AsyncKustoClient from azure.kusto.data.data_format import DataFormat, IngestionMappingKind from azure.kusto.data.exceptions import KustoServiceError from azure.kusto.data.streaming_response import FrameType from azure.kusto.ingest import ( QueuedIngestClient, KustoStreamingIngestClient, IngestionProperties, ColumnMapping, ValidationPolicy, ValidationOptions, ValidationImplications, ReportLevel, ReportMethod, FileDescriptor, BlobDescriptor, StreamDescriptor, ManagedStreamingIngestClient, ) @pytest.fixture(params=["ManagedStreaming", "NormalClient"]) def is_managed_streaming(request): return request.param == "ManagedStreaming" class TestE2E: """A class to define mappings to deft table.""" input_folder_path: str streaming_test_table: str test_streaming_data: list engine_cs: Optional[str] dm_cs: Optional[str] app_id: Optional[str] app_key: Optional[str] auth_id: Optional[str] test_db: Optional[str] client: KustoClient test_table: str current_count: int CHUNK_SIZE = 1024 @staticmethod def get_test_table_csv_mappings(): """A method to define csv mappings to test table.""" mappings = list() mappings.append(ColumnMapping(column_name="rownumber", column_type="int", ordinal=0)) mappings.append(ColumnMapping(column_name="rowguid", column_type="string", ordinal=1)) mappings.append(ColumnMapping(column_name="xdouble", column_type="real", ordinal=2)) mappings.append(ColumnMapping(column_name="xfloat", column_type="real", ordinal=3)) mappings.append(ColumnMapping(column_name="xbool", column_type="bool", ordinal=4)) mappings.append(ColumnMapping(column_name="xint16", column_type="int", ordinal=5)) mappings.append(ColumnMapping(column_name="xint32", column_type="int", ordinal=6)) mappings.append(ColumnMapping(column_name="xint64", column_type="long", ordinal=7)) mappings.append(ColumnMapping(column_name="xuint8", column_type="long", ordinal=8)) mappings.append(ColumnMapping(column_name="xuint16", column_type="long", ordinal=9)) mappings.append(ColumnMapping(column_name="xuint32", column_type="long", ordinal=10)) mappings.append(ColumnMapping(column_name="xuint64", column_type="long", ordinal=11)) mappings.append(ColumnMapping(column_name="xdate", column_type="datetime", ordinal=12)) mappings.append(ColumnMapping(column_name="xsmalltext", column_type="string", ordinal=13)) mappings.append(ColumnMapping(column_name="xtext", column_type="string", ordinal=14)) mappings.append(ColumnMapping(column_name="xnumberAsText", column_type="string", ordinal=15)) mappings.append(ColumnMapping(column_name="xtime", column_type="timespan", ordinal=16)) mappings.append(ColumnMapping(column_name="xtextWithNulls", column_type="string", ordinal=17)) mappings.append(ColumnMapping(column_name="xdynamicWithNulls", column_type="dynamic", ordinal=18)) return mappings @staticmethod def test_table_json_mappings(): """A method to define json mappings to test table.""" mappings = list() mappings.append(ColumnMapping(column_name="rownumber", path="$.rownumber", column_type="int")) mappings.append(ColumnMapping(column_name="rowguid", path="$.rowguid", column_type="string")) mappings.append(ColumnMapping(column_name="xdouble", path="$.xdouble", column_type="real")) mappings.append(ColumnMapping(column_name="xfloat", path="$.xfloat", column_type="real")) mappings.append(ColumnMapping(column_name="xbool", path="$.xbool", column_type="bool")) mappings.append(ColumnMapping(column_name="xint16", path="$.xint16", column_type="int")) mappings.append(ColumnMapping(column_name="xint32", path="$.xint32", column_type="int")) mappings.append(ColumnMapping(column_name="xint64", path="$.xint64", column_type="long")) mappings.append(ColumnMapping(column_name="xuint8", path="$.xuint8", column_type="long")) mappings.append(ColumnMapping(column_name="xuint16", path="$.xuint16", column_type="long")) mappings.append(ColumnMapping(column_name="xuint32", path="$.xuint32", column_type="long")) mappings.append(ColumnMapping(column_name="xuint64", path="$.xuint64", column_type="long")) mappings.append(ColumnMapping(column_name="xdate", path="$.xdate", column_type="datetime")) mappings.append(ColumnMapping(column_name="xsmalltext", path="$.xsmalltext", column_type="string")) mappings.append(ColumnMapping(column_name="xtext", path="$.xtext", column_type="string")) mappings.append(ColumnMapping(column_name="xnumberAsText", path="$.xnumberAsText", column_type="string")) mappings.append(ColumnMapping(column_name="xtime", path="$.xtime", column_type="timespan")) mappings.append(ColumnMapping(column_name="xtextWithNulls", path="$.xtextWithNulls", column_type="string")) mappings.append(ColumnMapping(column_name="xdynamicWithNulls", path="$.xdynamicWithNulls", column_type="dynamic")) return mappings @staticmethod def test_table_json_mapping_reference(): """A method to get json mappings reference to test table.""" return """'[' ' { "column" : "rownumber", "datatype" : "int", "Properties":{"Path":"$.rownumber"}},' ' { "column" : "rowguid", "datatype" : "string", "Properties":{"Path":"$.rowguid"}},' ' { "column" : "xdouble", "datatype" : "real", "Properties":{"Path":"$.xdouble"}},' ' { "column" : "xfloat", "datatype" : "real", "Properties":{"Path":"$.xfloat"}},' ' { "column" : "xbool", "datatype" : "bool", "Properties":{"Path":"$.xbool"}},' ' { "column" : "xint16", "datatype" : "int", "Properties":{"Path":"$.xint16"}},' ' { "column" : "xint32", "datatype" : "int", "Properties":{"Path":"$.xint32"}},' ' { "column" : "xint64", "datatype" : "long", "Properties":{"Path":"$.xint64"}},' ' { "column" : "xuint8", "datatype" : "long", "Properties":{"Path":"$.xuint8"}},' ' { "column" : "xuint16", "datatype" : "long", "Properties":{"Path":"$.xuint16"}},' ' { "column" : "xuint32", "datatype" : "long", "Properties":{"Path":"$.xuint32"}},' ' { "column" : "xuint64", "datatype" : "long", "Properties":{"Path":"$.xuint64"}},' ' { "column" : "xdate", "datatype" : "datetime", "Properties":{"Path":"$.xdate"}},' ' { "column" : "xsmalltext", "datatype" : "string", "Properties":{"Path":"$.xsmalltext"}},' ' { "column" : "xtext", "datatype" : "string", "Properties":{"Path":"$.xtext"}},' ' { "column" : "xnumberAsText", "datatype" : "string", "Properties":{"Path":"$.rowguid"}},' ' { "column" : "xtime", "datatype" : "timespan", "Properties":{"Path":"$.xtime"}},' ' { "column" : "xtextWithNulls", "datatype" : "string", "Properties":{"Path":"$.xtextWithNulls"}},' ' { "column" : "xdynamicWithNulls", "datatype" : "dynamic", "Properties":{"Path":"$.xdynamicWithNulls"}},' ']'""" @staticmethod def get_file_path() -> str: current_dir = os.getcwd() path_parts = ["azure-kusto-ingest", "tests", "input"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) return os.path.join(current_dir, *missing_path_parts) @classmethod def engine_kcsb_from_env(cls) -> KustoConnectionStringBuilder: if all([cls.app_id, cls.app_key, cls.auth_id]): return KustoConnectionStringBuilder.with_aad_application_key_authentication(cls.engine_cs, cls.app_id, cls.app_key, cls.auth_id) else: return KustoConnectionStringBuilder.with_interactive_login(cls.engine_cs) @classmethod def dm_kcsb_from_env(cls) -> KustoConnectionStringBuilder: if all([cls.app_id, cls.app_key, cls.auth_id]): return KustoConnectionStringBuilder.with_aad_application_key_authentication(cls.dm_cs, cls.app_id, cls.app_key, cls.auth_id) else: return KustoConnectionStringBuilder.with_interactive_login(cls.dm_cs) @classmethod def setup_class(cls): # DM CS can be composed from engine CS cls.engine_cs = os.environ.get("ENGINE_CONNECTION_STRING") cls.dm_cs = os.environ.get("DM_CONNECTION_STRING") or cls.engine_cs.replace("//", "//ingest-") cls.app_id = os.environ.get("APP_ID") cls.app_key = os.environ.get("APP_KEY") cls.auth_id = os.environ.get("AUTH_ID") cls.test_db = os.environ.get("TEST_DATABASE") cls.test_blob = os.environ.get("TEST_BLOB") if not all([cls.engine_cs, cls.dm_cs, cls.test_db]): raise unittest.SkipTest("E2E environment is missing") # Init clients python_version = "_".join([str(v) for v in sys.version_info[:3]]) cls.test_table = "python_test_{0}_{1}_{2}".format(python_version, str(int(time.time())), random.randint(1, 100000)) cls.streaming_test_table = "BigChunkus" cls.streaming_test_table_query = cls.streaming_test_table + " | order by timestamp" cls.client = KustoClient(cls.engine_kcsb_from_env()) cls.ingest_client = QueuedIngestClient(cls.dm_kcsb_from_env()) cls.streaming_ingest_client = KustoStreamingIngestClient(cls.engine_kcsb_from_env()) cls.managed_streaming_ingest_client = ManagedStreamingIngestClient(cls.engine_kcsb_from_env(), cls.dm_kcsb_from_env()) cls.input_folder_path = cls.get_file_path() cls.csv_file_path = os.path.join(cls.input_folder_path, "dataset.csv") cls.tsv_file_path = os.path.join(cls.input_folder_path, "dataset.tsv") cls.zipped_csv_file_path = os.path.join(cls.input_folder_path, "dataset.csv.gz") cls.json_file_path = os.path.join(cls.input_folder_path, "dataset.json") cls.zipped_json_file_path = os.path.join(cls.input_folder_path, "dataset.jsonz.gz") with open(os.path.join(cls.input_folder_path, "big.json")) as f: cls.test_streaming_data = json.load(f) cls.current_count = 0 cls.client.execute( cls.test_db, f".create table {cls.test_table} (rownumber: int, rowguid: string, xdouble: real, xfloat: real, xbool: bool, xint16: int, xint32: int, xint64: long, xuint8: long, xuint16: long, xuint32: long, xuint64: long, xdate: datetime, xsmalltext: string, xtext: string, xnumberAsText: string, xtime: timespan, xtextWithNulls: string, xdynamicWithNulls: dynamic)", ) cls.client.execute(cls.test_db, f".create table {cls.test_table} ingestion json mapping 'JsonMapping' {cls.test_table_json_mapping_reference()}") cls.client.execute(cls.test_db, f".alter table {cls.test_table} policy streamingingestion enable ") # Clear the cache to guarantee that subsequent streaming ingestion requests incorporate database and table schema changes # See https://docs.microsoft.com/azure/data-explorer/kusto/management/data-ingestion/clear-schema-cache-command cls.client.execute(cls.test_db, ".clear database cache streamingingestion schema") @classmethod def teardown_class(cls): cls.client.execute(cls.test_db, ".drop table {} ifexists".format(cls.test_table)) @classmethod async def get_async_client(cls) -> AsyncKustoClient: return AsyncKustoClient(cls.engine_kcsb_from_env()) # assertions @classmethod async def assert_rows_added(cls, expected: int, timeout=60): actual = 0 while timeout > 0: time.sleep(1) timeout -= 1 try: command = "{} | count".format(cls.test_table) response = cls.client.execute(cls.test_db, command) async_client = await cls.get_async_client() response_from_async = await async_client.execute(cls.test_db, command) except KustoServiceError: continue if response is not None: row = response.primary_results[0][0] row_async = response_from_async.primary_results[0][0] actual = int(row["Count"]) - cls.current_count # this is done to allow for data to arrive properly if actual >= expected: assert row_async == row, "Mismatch answers between async('{0}') and sync('{1}') clients".format(row_async, row) break cls.current_count += actual assert actual == expected, "Row count expected = {0}, while actual row count = {1}".format(expected, actual) @staticmethod def normalize_row(row): result = [] for r in row: if type(r) == bool: result.append(int(r)) elif type(r) == datetime: result.append(r.strftime("%Y-%m-%dT%H:%M:%SZ")) else: result.append(r) return result def test_streaming_query(self): result = self.client.execute_streaming_query(self.test_db, self.streaming_test_table_query + ";" + self.streaming_test_table_query) counter = 0 result.set_skip_incomplete_tables(True) for primary in result.iter_primary_results(): counter += 1 for row in self.test_streaming_data: assert row == self.normalize_row(next(primary).to_list()) assert counter == 2 assert result.finished assert result.errors_count == 0 assert result.get_exceptions() == [] @pytest.mark.asyncio async def test_streaming_query_async(self): async with await self.get_async_client() as client: result = await client.execute_streaming_query(self.test_db, self.streaming_test_table_query + ";" + self.streaming_test_table_query) counter = 0 result.set_skip_incomplete_tables(True) async for primary in result.iter_primary_results(): counter += 1 streaming_data_iter = iter(self.test_streaming_data) async for row in primary: expected_row = next(streaming_data_iter, None) if expected_row is None: break assert expected_row == self.normalize_row(row.to_list()) assert counter == 2 assert result.finished assert result.errors_count == 0 assert result.get_exceptions() == [] def test_streaming_query_internal(self): frames = self.client._execute_streaming_query_parsed(self.test_db, self.streaming_test_table_query) initial_frame = next(frames) expected_initial_frame = { "FrameType": FrameType.DataSetHeader, "IsProgressive": False, "Version": "v2.0", } assert initial_frame == expected_initial_frame query_props = next(frames) assert query_props["FrameType"] == FrameType.DataTable assert query_props["TableKind"] == WellKnownDataSet.QueryProperties.value assert type(query_props["Columns"]) == list assert type(query_props["Rows"]) == list assert len(query_props["Rows"][0]) == len(query_props["Columns"]) primary_result = next(frames) assert primary_result["FrameType"] == FrameType.DataTable assert primary_result["TableKind"] == WellKnownDataSet.PrimaryResult.value assert type(primary_result["Columns"]) == list assert type(primary_result["Rows"]) != list row = next(primary_result["Rows"]) assert len(row) == len(primary_result["Columns"]) @pytest.mark.asyncio async def test_streaming_query_internal_async(self): async with await self.get_async_client() as client: frames = await client._execute_streaming_query_parsed(self.test_db, self.streaming_test_table_query) frames.__aiter__() initial_frame = await frames.__anext__() expected_initial_frame = { "FrameType": FrameType.DataSetHeader, "IsProgressive": False, "Version": "v2.0", } assert initial_frame == expected_initial_frame query_props = await frames.__anext__() assert query_props["FrameType"] == FrameType.DataTable assert query_props["TableKind"] == WellKnownDataSet.QueryProperties.value assert type(query_props["Columns"]) == list assert type(query_props["Rows"]) == list assert len(query_props["Rows"][0]) == len(query_props["Columns"]) primary_result = await frames.__anext__() assert primary_result["FrameType"] == FrameType.DataTable assert primary_result["TableKind"] == WellKnownDataSet.PrimaryResult.value assert type(primary_result["Columns"]) == list assert type(primary_result["Rows"]) != list row = await primary_result["Rows"].__anext__() assert len(row) == len(primary_result["Columns"]) @pytest.mark.asyncio async def test_csv_ingest_existing_table(self, is_managed_streaming): csv_ingest_props = IngestionProperties( self.test_db, self.test_table, data_format=DataFormat.CSV, column_mappings=self.get_test_table_csv_mappings(), report_level=ReportLevel.FailuresAndSuccesses, flush_immediately=True, ) client = self.streaming_ingest_client if is_managed_streaming else self.ingest_client for f in [self.csv_file_path, self.zipped_csv_file_path]: client.ingest_from_file(f, csv_ingest_props) await self.assert_rows_added(20) @pytest.mark.asyncio async def test_json_ingest_existing_table(self): json_ingestion_props = IngestionProperties( self.test_db, self.test_table, flush_immediately=True, data_format=DataFormat.JSON, column_mappings=self.test_table_json_mappings(), report_level=ReportLevel.FailuresAndSuccesses, ) for f in [self.json_file_path, self.zipped_json_file_path]: self.ingest_client.ingest_from_file(f, json_ingestion_props) await self.assert_rows_added(4) @pytest.mark.asyncio async def test_ingest_complicated_props(self): validation_policy = ValidationPolicy( validation_options=ValidationOptions.ValidateCsvInputConstantColumns, validation_implications=ValidationImplications.Fail ) json_ingestion_props = IngestionProperties( self.test_db, self.test_table, data_format=DataFormat.JSON, column_mappings=self.test_table_json_mappings(), additional_tags=["a", "b"], ingest_if_not_exists=["aaaa", "bbbb"], ingest_by_tags=["ingestByTag"], drop_by_tags=["drop", "drop-by"], flush_immediately=False, report_level=ReportLevel.FailuresAndSuccesses, report_method=ReportMethod.Queue, validation_policy=validation_policy, ) file_paths = [self.json_file_path, self.zipped_json_file_path] fds = [FileDescriptor(fp, 0, uuid.uuid4()) for fp in file_paths] for fd in fds: self.ingest_client.ingest_from_file(fd, json_ingestion_props) await self.assert_rows_added(4) @pytest.mark.asyncio async def test_ingest_from_stream(self, is_managed_streaming): validation_policy = ValidationPolicy( validation_options=ValidationOptions.ValidateCsvInputConstantColumns, validation_implications=ValidationImplications.Fail ) json_ingestion_props = IngestionProperties( self.test_db, self.test_table, data_format=DataFormat.JSON, column_mappings=self.test_table_json_mappings(), additional_tags=["a", "b"], ingest_if_not_exists=["aaaa", "bbbb"], ingest_by_tags=["ingestByTag"], drop_by_tags=["drop", "drop-by"], flush_immediately=False, report_level=ReportLevel.FailuresAndSuccesses, report_method=ReportMethod.Queue, validation_policy=validation_policy, ) text = io.StringIO(pathlib.Path(self.json_file_path).read_text()) zipped = io.BytesIO(pathlib.Path(self.zipped_json_file_path).read_bytes()) client = self.managed_streaming_ingest_client if is_managed_streaming else self.ingest_client client.ingest_from_stream(text, json_ingestion_props) client.ingest_from_stream(StreamDescriptor(zipped, is_compressed=True), json_ingestion_props) await self.assert_rows_added(4) @pytest.mark.asyncio async def test_json_ingestion_ingest_by_tag(self): json_ingestion_props = IngestionProperties( self.test_db, self.test_table, data_format=DataFormat.JSON, column_mappings=self.test_table_json_mappings(), ingest_if_not_exists=["ingestByTag"], report_level=ReportLevel.FailuresAndSuccesses, drop_by_tags=["drop", "drop-by"], flush_immediately=True, ) for f in [self.json_file_path, self.zipped_json_file_path]: self.ingest_client.ingest_from_file(f, json_ingestion_props) await self.assert_rows_added(0) @pytest.mark.asyncio async def test_tsv_ingestion_csv_mapping(self): tsv_ingestion_props = IngestionProperties( self.test_db, self.test_table, flush_immediately=True, data_format=DataFormat.TSV, column_mappings=self.get_test_table_csv_mappings(), report_level=ReportLevel.FailuresAndSuccesses, ) self.ingest_client.ingest_from_file(self.tsv_file_path, tsv_ingestion_props) await self.assert_rows_added(10) @pytest.mark.asyncio async def test_ingest_blob(self): if not self.test_blob: pytest.skip("Provide blob SAS uri with 'dataset.csv'") csv_ingest_props = IngestionProperties( self.test_db, self.test_table, data_format=DataFormat.CSV, column_mappings=self.get_test_table_csv_mappings(), report_level=ReportLevel.FailuresAndSuccesses, flush_immediately=True, ) blob_len = 1578 self.ingest_client.ingest_from_blob(BlobDescriptor(self.test_blob, blob_len), csv_ingest_props) await self.assert_rows_added(10) # Don't provide size hint self.ingest_client.ingest_from_blob(BlobDescriptor(self.test_blob, size=None), csv_ingest_props) await self.assert_rows_added(10) @pytest.mark.asyncio async def test_streaming_ingest_from_opened_file(self, is_managed_streaming): ingestion_properties = IngestionProperties(database=self.test_db, table=self.test_table, data_format=DataFormat.CSV) client = self.managed_streaming_ingest_client if is_managed_streaming else self.streaming_ingest_client with open(self.csv_file_path, "r") as stream: client.ingest_from_stream(stream, ingestion_properties=ingestion_properties) await self.assert_rows_added(10, timeout=120) @pytest.mark.asyncio async def test_streaming_ingest_from_csv_file(self): ingestion_properties = IngestionProperties(database=self.test_db, table=self.test_table, flush_immediately=True, data_format=DataFormat.CSV) for f in [self.csv_file_path, self.zipped_csv_file_path]: self.streaming_ingest_client.ingest_from_file(f, ingestion_properties=ingestion_properties) await self.assert_rows_added(20, timeout=120) @pytest.mark.asyncio async def test_streaming_ingest_from_json_file(self): ingestion_properties = IngestionProperties( database=self.test_db, table=self.test_table, flush_immediately=True, data_format=DataFormat.JSON, ingestion_mapping_reference="JsonMapping", ingestion_mapping_kind=IngestionMappingKind.JSON, ) for f in [self.json_file_path, self.zipped_json_file_path]: self.streaming_ingest_client.ingest_from_file(f, ingestion_properties=ingestion_properties) await self.assert_rows_added(4, timeout=120) @pytest.mark.asyncio async def test_streaming_ingest_from_csv_io_streams(self): ingestion_properties = IngestionProperties(database=self.test_db, table=self.test_table, data_format=DataFormat.CSV) byte_sequence = b'0,00000000-0000-0000-0001-020304050607,0,0,0,0,0,0,0,0,0,0,2014-01-01T01:01:01.0000000Z,Zero,"Zero",0,00:00:00,,null' bytes_stream = io.BytesIO(byte_sequence) self.streaming_ingest_client.ingest_from_stream(bytes_stream, ingestion_properties=ingestion_properties) str_sequence = '0,00000000-0000-0000-0001-020304050607,0,0,0,0,0,0,0,0,0,0,2014-01-01T01:01:01.0000000Z,Zero,"Zero",0,00:00:00,,null' str_stream = io.StringIO(str_sequence) self.streaming_ingest_client.ingest_from_stream(str_stream, ingestion_properties=ingestion_properties) await self.assert_rows_added(2, timeout=120) @pytest.mark.asyncio async def test_streaming_ingest_from_json_io_streams(self): ingestion_properties = IngestionProperties( database=self.test_db, table=self.test_table, data_format=DataFormat.JSON, flush_immediately=True, ingestion_mapping_reference="JsonMapping", ingestion_mapping_kind=IngestionMappingKind.JSON, ) byte_sequence = b'{"rownumber": 0, "rowguid": "00000000-0000-0000-0001-020304050607", "xdouble": 0.0, "xfloat": 0.0, "xbool": 0, "xint16": 0, "xint32": 0, "xint64": 0, "xunit8": 0, "xuint16": 0, "xunit32": 0, "xunit64": 0, "xdate": "2014-01-01T01:01:01Z", "xsmalltext": "Zero", "xtext": "Zero", "xnumberAsText": "0", "xtime": "00:00:00", "xtextWithNulls": null, "xdynamicWithNulls": ""}' bytes_stream = io.BytesIO(byte_sequence) self.streaming_ingest_client.ingest_from_stream(bytes_stream, ingestion_properties=ingestion_properties) str_sequence = '{"rownumber": 0, "rowguid": "00000000-0000-0000-0001-020304050607", "xdouble": 0.0, "xfloat": 0.0, "xbool": 0, "xint16": 0, "xint32": 0, "xint64": 0, "xunit8": 0, "xuint16": 0, "xunit32": 0, "xunit64": 0, "xdate": "2014-01-01T01:01:01Z", "xsmalltext": "Zero", "xtext": "Zero", "xnumberAsText": "0", "xtime": "00:00:00", "xtextWithNulls": null, "xdynamicWithNulls": ""}' str_stream = io.StringIO(str_sequence) self.streaming_ingest_client.ingest_from_stream(str_stream, ingestion_properties=ingestion_properties) await self.assert_rows_added(2, timeout=120) @pytest.mark.asyncio async def test_streaming_ingest_from_dataframe(self): from pandas import DataFrame fields = [ "rownumber", "rowguid", "xdouble", "xfloat", "xbool", "xint16", "xint32", "xint64", "xunit8", "xuint16", "xunit32", "xunit64", "xdate", "xsmalltext", "xtext", "xnumberAsText", "xtime", "xtextWithNulls", "xdynamicWithNulls", ] rows = [ [0, "00000000-0000-0000-0001-020304050607", 0.0, 0.0, 0, 0, 0, 0, 0, 0, 0, 0, "2014-01-01T01:01:01Z", "Zero", "Zero", "0", "00:00:00", None, ""] ] df = DataFrame(data=rows, columns=fields) ingestion_properties = IngestionProperties(database=self.test_db, table=self.test_table, flush_immediately=True, data_format=DataFormat.CSV) self.ingest_client.ingest_from_dataframe(df, ingestion_properties) await self.assert_rows_added(1, timeout=120) def test_cloud_info(self): cloud_info = CloudSettings.get_cloud_info_for_cluster(self.engine_cs) assert cloud_info is not CloudSettings.DEFAULT_CLOUD assert cloud_info == CloudSettings.DEFAULT_CLOUD assert cloud_info is CloudSettings.get_cloud_info_for_cluster(self.engine_cs) def test_cloud_info_404(self): cloud_info = CloudSettings.get_cloud_info_for_cluster("https://www.microsoft.com") assert cloud_info is CloudSettings.DEFAULT_CLOUD azure-kusto-python-3.0.1/azure-kusto-ingest/tests/input/000077500000000000000000000000001417222763000234105ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-ingest/tests/input/__init__.py000066400000000000000000000000001417222763000255070ustar00rootroot00000000000000azure-kusto-python-3.0.1/azure-kusto-ingest/tests/input/big.json000066400000000000000000002444461417222763000250620ustar00rootroot00000000000000[ [ 952.45224445872259, 0, "Kibana Airlines", "Shanghai Hongqiao International Airport", "SHA", "Shanghai", "CN", { "lat": "31.19790077", "lon": "121.3359985" }, "SE-BD", "Rain", 9243.8109634707889, 5743.8378391883825, 0, 0, "No Delay", "GDZWNB0", 12.838626338153874, 770.31758028923241, "London Gatwick Airport", "LGW", "London", "GB", { "lat": "51.14810181", "lon": "-0.190277994" }, "GB-ENG", "Clear", 6, "2018-02-11T23:50:12Z" ], [ 1028.7682366113208, 0, "Logstash Airways", "Tokyo Haneda International Airport", "HND", "Tokyo", "JP", { "lat": "35.552299", "lon": "139.779999" }, "SE-BD", "Rain", 7017.3466765227058, 4360.3770707336062, 0, 0, "No Delay", "OKZ5QTE", 5.8477888971022542, 350.86733382613528, "Olenya Air Base", "XLMO", "Olenegorsk", "RU", { "lat": "68.15180206", "lon": "33.46390152" }, "RU-MUR", "Clear", 6, "2018-02-11T23:46:01Z" ], [ 1091.8802959347431, 0, "JetBeats", "King Shaka International Airport", "DUR", "Durban", "ZA", { "lat": "-29.61444444", "lon": "31.11972222" }, "SE-BD", "Clear", 8504.6098431600785, 5284.5195577577433, 0, 0, "No Delay", "RM652PY", 11.81195811550011, 708.71748693000654, "Genoa Cristoforo Colombo Airport", "GE01", "Genova", "IT", { "lat": "44.4133", "lon": "8.8375" }, "IT-42", "Thunder & Lightning", 6, "2018-02-11T23:29:45Z" ], [ 440.32475229026517, 1, "Kibana Airlines", "Seattle Tacoma International Airport", "SEA", "Seattle", "US", { "lat": "47.44900131", "lon": "-122.3089981" }, "US-WA", "Thunder & Lightning", 3419.5367460130756, 2124.8016247695182, 0, 0, "No Delay", "KUO775Y", 5.1811162818379932, 310.86697691027962, "Pittsburgh International Airport", "PIT", "Pittsburgh", "US", { "lat": "40.49150085", "lon": "-80.23290253" }, "US-PA", "Clear", 6, "2018-02-11T23:27:00Z" ], [ 908.84073603059437, 0, "JetBeats", "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Sunny", 6804.6889820233691, 4228.2377055641109, 0, 0, "No Delay", "SE3S0ZP", 7.0882176896076761, 425.29306137646057, "Rajiv Gandhi International Airport", "HYD", "Hyderabad", "IN", { "lat": "17.23131752", "lon": "78.42985535" }, "SE-BD", "Heavy Fog", 6, "2018-02-11T23:21:27Z" ], [ 760.33982094876387, 1, "ES-Air", "Vienna International Airport", "VIE", "Vienna", "AT", { "lat": "48.11029816", "lon": "16.56970024" }, "AT-9", "Cloudy", 657.92022778356056, 408.81267633492934, 0, 0, "No Delay", "MY1XRHF", 0.6091853960958894, 36.551123765753367, "Malpensa International Airport", "MI12", "Milan", "IT", { "lat": "45.6306", "lon": "8.72811" }, "IT-25", "Thunder & Lightning", 6, "2018-02-11T23:12:33Z" ], [ 1000.6070389356696, 0, "Logstash Airways", "Montreal / Pierre Elliott Trudeau International Airport", "YUL", "Montreal", "CA", { "lat": "45.47060013", "lon": "-73.74079895" }, "CA-QC", "Heavy Fog", 10598.641992305174, 6585.690810855338, 0, 0, "No Delay", "JQGOQ0J", 9.2970543792150657, 557.82326275290393, "Incheon International Airport", "ICN", "Seoul", "KR", { "lat": "37.46910095", "lon": "126.4509964" }, "SE-BD", "Hail", 6, "2018-02-11T23:04:08Z" ], [ 788.63906303459316, 0, "Kibana Airlines", "London Gatwick Airport", "LGW", "London", "GB", { "lat": "51.14810181", "lon": "-0.190277994" }, "GB-ENG", "Sunny", 1407.2159452218614, 874.40344961789481, 0, 0, "No Delay", "VAI7N23", 1.6752570776450733, 100.51542465870439, "Leonardo da Vinci - Fiumicino Airport", "FCO", "Rome", "IT", { "lat": "41.8002778", "lon": "12.2388889" }, "SE-BD", "Clear", 6, "2018-02-11T23:01:03Z" ], [ 1155.3405895759547, 0, "Kibana Airlines", "OR Tambo International Airport", "JNB", "Johannesburg", "ZA", { "lat": "-26.1392", "lon": "28.246" }, "SE-BD", "Sunny", 11657.089629455511, 7243.3796810722324, 0, 0, "No Delay", "LO0I4OG", 14.944986704430143, 896.69920226580859, "Brisbane International Airport", "BNE", "Brisbane", "AU", { "lat": "-27.38419914", "lon": "153.1170044" }, "SE-BD", "Sunny", 6, "2018-02-11T22:58:25Z" ], [ 929.83479006014068, 0, "Logstash Airways", "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Rain", 291.90172192067166, 181.37932096597845, 0, 0, "No Delay", "GR9Y0H6", 0.27027937214877007, 16.216762328926205, "Verona Villafranca Airport", "VR10", "Verona", "IT", { "lat": "45.395699", "lon": "10.8885" }, "IT-34", "Cloudy", 6, "2018-02-11T22:53:35Z" ], [ 842.29769420000446, 0, "Kibana Airlines", "Gimpo International Airport", "GMP", "Seoul", "KR", { "lat": "37.5583", "lon": "126.791" }, "SE-BD", "Clear", 7696.8683735093864, 4782.6122777413566, 0, 0, "No Delay", "SF9YY0W", 11.661921778044524, 699.71530668267144, "Brisbane International Airport", "BNE", "Brisbane", "AU", { "lat": "-27.38419914", "lon": "153.1170044" }, "SE-BD", "Heavy Fog", 6, "2018-02-11T22:44:14Z" ], [ 707.85021485573384, 0, "Kibana Airlines", "Cagliari Elmas Airport", "CA07", "Cagliari", "IT", { "lat": "39.251499", "lon": "9.05428" }, "IT-88", "Sunny", 7582.8660844200194, 4711.7745394521116, 0, 0, "No Delay", "IBSRCT5", 7.8988188379375206, 473.92913027625121, "Austin Straubel International Airport", "GRB", "Green Bay", "US", { "lat": "44.48509979", "lon": "-88.12960052" }, "US-WI", "Clear", 6, "2018-02-11T22:40:56Z" ], [ 1034.3357332067189, 0, "ES-Air", "Kempegowda International Airport", "BLR", "Bangalore", "IN", { "lat": "13.1979", "lon": "77.706299" }, "SE-BD", "Damaging Wind", 6248.0121637224674, 3882.3347672855939, 0, 0, "No Delay", "UXUB9IV", 8.67779467183676, 520.66768031020558, "Itami Airport", "ITM", "Osaka", "JP", { "lat": "34.78549957", "lon": "135.4380035" }, "SE-BD", "Cloudy", 6, "2018-02-11T22:17:15Z" ], [ 1077.0333368503507, 0, "Kibana Airlines", "Pisa International Airport", "PI05", "Pisa", "IT", { "lat": "43.683899", "lon": "10.3927" }, "IT-52", "Sunny", 7058.14624061129, 4385.7287445140937, 0, 0, "No Delay", "4WKID46", 9.04890543668114, 542.93432620086844, "Norfolk International Airport", "ORF", "Norfolk", "US", { "lat": "36.89459991", "lon": "-76.20120239" }, "US-VA", "Thunder & Lightning", 6, "2018-02-11T22:15:05Z" ], [ 294.55761631493806, 0, "ES-Air", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Clear", 0.0, 0.0, 1, 105, "Security Delay", "MIK9FQC", 1.75, 105.0, "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Clear", 6, "2018-02-11T22:06:14Z" ], [ 839.02205064681084, 0, "ES-Air", "Stockholm-Arlanda Airport", "ARN", "Stockholm", "SE", { "lat": "59.65190125", "lon": "17.91860008" }, "SE-AB", "Rain", 1223.803785060335, 760.4364169875023, 0, 0, "No Delay", "MPU4VJH", 1.1998076324120932, 71.9884579447256, "Sheremetyevo International Airport", "SVO", "Moscow", "RU", { "lat": "55.972599", "lon": "37.4146" }, "RU-MOS", "Clear", 6, "2018-02-11T22:06:14Z" ], [ 1067.5172474339875, 0, "Logstash Airways", "Leonardo da Vinci___Fiumicino Airport", "RM11", "Rome", "IT", { "lat": "41.8002778", "lon": "12.2388889" }, "IT-62", "Clear", 9813.843354728655, 6098.0395457581808, 1, 270, "Late Aircraft Delay", "D940ONX", 14.722753494509016, 883.36520967054093, "Chubu Centrair International Airport", "NGO", "Tokoname", "JP", { "lat": "34.85839844", "lon": "136.8049927" }, "SE-BD", "Sunny", 6, "2018-02-11T21:44:41Z" ], [ 953.12119557978758, 0, "Logstash Airways", "New Chitose Airport", "CTS", "Chitose / Tomakomai", "JP", { "lat": "42.77519989", "lon": "141.6920013" }, "SE-BD", "Clear", 17010.351663163041, 10569.742493315935, 0, 0, "No Delay", "7WHK95X", 21.808143157901334, 1308.48858947408, "Comodoro Arturo Merino Benitez International Airport", "SCL", "Santiago", "CL", { "lat": "-33.39300156", "lon": "-70.78579712" }, "SE-BD", "Clear", 6, "2018-02-11T21:41:38Z" ], [ 1188.0474367030561, 0, "Kibana Airlines", "Sydney Kingsford Smith International Airport", "SYD", "Sydney", "AU", { "lat": "-33.94609833", "lon": "151.177002" }, "SE-BD", "Rain", 16609.582803034431, 10320.716268886223, 0, 0, "No Delay", "DHGBL3D", 21.294336926967219, 1277.6602156180331, "Cagliari Elmas Airport", "CA07", "Cagliari", "IT", { "lat": "39.251499", "lon": "9.05428" }, "IT-88", "Damaging Wind", 6, "2018-02-11T21:39:01Z" ], [ 444.73316181585278, 0, "ES-Air", "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Clear", 0.0, 0.0, 0, 0, "No Delay", "2DF7FDQ", 0.0, 0.0, "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Sunny", 6, "2018-02-11T21:16:59Z" ], [ 514.763998895086, 0, "ES-Air", "Naples International Airport", "NA01", "Naples", "IT", { "lat": "40.886002", "lon": "14.2908" }, "IT-72", "Hail", 4155.0468641243951, 2581.826423762971, 0, 0, "No Delay", "RT11C1Y", 3.4625390534369962, 207.75234320621976, "Abu Dhabi International Airport", "AUH", "Abu Dhabi", "AE", { "lat": "24.43300056", "lon": "54.65110016" }, "SE-BD", "Rain", 6, "2018-02-11T20:58:57Z" ], [ 355.32975259018224, 0, "Kibana Airlines", "Ciampino___G. B. Pastine International Airport", "RM12", "Rome", "IT", { "lat": "41.7994", "lon": "12.5949" }, "IT-62", "Sunny", 0.0, 0.0, 0, 0, "No Delay", "Z2Q0URG", 0.0, 0.0, "Ciampino___G. B. Pastine International Airport", "RM12", "Rome", "IT", { "lat": "41.7994", "lon": "12.5949" }, "IT-62", "Rain", 6, "2018-02-11T20:55:06Z" ], [ 1181.5361621116438, 1, "ES-Air", "Mariscal Sucre International Airport", "UIO", "Quito", "EC", { "lat": "-0.129166667", "lon": "-78.3575" }, "EC-P", "Heavy Fog", 9799.8133772472956, 6089.3217219235257, 0, 0, "No Delay", "X19GQ3B", 8.16651114770608, 489.99066886236477, "Frankfurt am Main Airport", "FRA", "Frankfurt am Main", "DE", { "lat": "50.033333", "lon": "8.570556" }, "DE-HE", "Sunny", 6, "2018-02-11T20:55:06Z" ], [ 375.54305249369128, 0, "ES-Air", "Vienna International Airport", "VIE", "Vienna", "AT", { "lat": "48.11029816", "lon": "16.56970024" }, "AT-9", "Damaging Wind", 0.0, 0.0, 1, 360, "Weather Delay", "MX0N754", 6.0, 360.0, "Vienna International Airport", "VIE", "Vienna", "AT", { "lat": "48.11029816", "lon": "16.56970024" }, "AT-9", "Heavy Fog", 6, "2018-02-11T20:53:44Z" ], [ 867.231028821183, 0, "ES-Air", "Shanghai Pudong International Airport", "PVG", "Shanghai", "CN", { "lat": "31.14340019", "lon": "121.8050003" }, "SE-BD", "Damaging Wind", 9287.1298633138267, 5770.75495563026, 0, 0, "No Delay", "WK1D6D7", 8.5991943178831729, 515.95165907299042, "Charles de Gaulle International Airport", "CDG", "Paris", "FR", { "lat": "49.01279831", "lon": "2.549999952" }, "FR-J", "Clear", 6, "2018-02-11T20:51:31Z" ], [ 1080.4462785162759, 0, "Logstash Airways", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Rain", 8058.5817527533545, 5007.3705514503763, 0, 0, "No Delay", "I0G6LLT", 6.7154847939611289, 402.92908763766775, "Pisa International Airport", "PI05", "Pisa", "IT", { "lat": "43.683899", "lon": "10.3927" }, "IT-52", "Sunny", 6, "2018-02-11T20:42:25Z" ], [ 511.71995921233889, 0, "ES-Air", "Cologne Bonn Airport", "CGN", "Cologne", "DE", { "lat": "50.86589813", "lon": "7.142739773" }, "DE-NW", "Clear", 769.26031174690661, 477.99619705103851, 1, 195, "NAS Delay", "TZHHKZ1", 3.8910502597890888, 233.46301558734533, "Bologna Guglielmo Marconi Airport", "BO08", "Bologna", "IT", { "lat": "44.5354", "lon": "11.2887" }, "IT-45", "Cloudy", 6, "2018-02-11T20:30:08Z" ], [ 602.62088547217445, 0, "Kibana Airlines", "San Diego International Airport", "SAN", "San Diego", "US", { "lat": "32.73360062", "lon": "-117.1900024" }, "US-CA", "Rain", 13618.993094316242, 8462.4499760873, 0, 0, "No Delay", "3A1PBBE", 11.946485170452842, 716.78911022717057, "Abu Dhabi International Airport", "AUH", "Abu Dhabi", "AE", { "lat": "24.43300056", "lon": "54.65110016" }, "SE-BD", "Sunny", 6, "2018-02-11T20:24:22Z" ], [ 577.72386784829507, 0, "Logstash Airways", "San Antonio International Airport", "SAT", "San Antonio", "US", { "lat": "29.53370094", "lon": "-98.46980286" }, "US-TX", "Rain", 10505.11710358143, 6527.5771392452007, 0, 0, "No Delay", "F6OPZ0P", 9.2150150031416054, 552.90090018849628, "Ataturk International Airport", "IST", "Istanbul", "TR", { "lat": "40.97689819", "lon": "28.81459999" }, "TR-34", "Sunny", 6, "2018-02-11T20:19:55Z" ], [ 795.90527761085707, 0, "Kibana Airlines", "Malpensa International Airport", "MI12", "Milan", "IT", { "lat": "45.6306", "lon": "8.72811" }, "IT-25", "Sunny", 9618.7648755421524, 5976.8233985662182, 0, 0, "No Delay", "9D9X8N4", 8.9062637736501422, 534.37582641900849, "Itami Airport", "ITM", "Osaka", "JP", { "lat": "34.78549957", "lon": "135.4380035" }, "SE-BD", "Sunny", 6, "2018-02-11T20:10:13Z" ], [ 1078.6406790975925, 0, "Logstash Airways", "Jorge Chavez International Airport", "LIM", "Lima", "PE", { "lat": "-12.0219", "lon": "-77.114304" }, "SE-BD", "Sunny", 10511.438749435607, 6531.5052278665135, 1, 225, "Late Aircraft Delay", "652J76O", 17.226203524917445, 1033.5722114950468, "Amsterdam Airport Schiphol", "AMS", "Amsterdam", "NL", { "lat": "52.30860138", "lon": "4.76388979" }, "NL-NH", "Rain", 6, "2018-02-11T20:08:25Z" ], [ 840.46119039508471, 0, "Kibana Airlines", "Ukrainka Air Base", "XHBU", "Belogorsk", "RU", { "lat": "51.169997", "lon": "128.445007" }, "RU-AMU", "Rain", 2294.2233422135751, 1425.56429340997, 1, 45, "NAS Delay", "O9N0WNY", 3.139815981472474, 188.38895888834844, "Shanghai Hongqiao International Airport", "SHA", "Shanghai", "CN", { "lat": "31.19790077", "lon": "121.3359985" }, "SE-BD", "Rain", 6, "2018-02-11T20:03:31Z" ], [ 891.14456428013091, 0, "Logstash Airways", "Jeju International Airport", "CJU", "Jeju City", "KR", { "lat": "33.51129913", "lon": "126.4929962" }, "SE-BD", "Rain", 836.66289131553981, 519.87821827747189, 0, 0, "No Delay", "5MK5DV7", 0.6972190760962832, 41.83314456577699, "Itami Airport", "ITM", "Osaka", "JP", { "lat": "34.78549957", "lon": "135.4380035" }, "SE-BD", "Sunny", 6, "2018-02-11T20:03:31Z" ], [ 313.42919532459871, 0, "JetBeats", "Tucson International Airport", "TUS", "Tucson", "US", { "lat": "32.11610031", "lon": "-110.9410019" }, "US-AZ", "Cloudy", 3057.7707791247972, 1900.0106746132567, 0, 0, "No Delay", "HNEWZ0B", 3.1851778949216638, 191.11067369529982, "Miami International Airport", "MIA", "Miami", "US", { "lat": "25.79319954", "lon": "-80.29060364" }, "US-FL", "Sunny", 6, "2018-02-11T19:54:11Z" ], [ 1053.5419730483895, 0, "JetBeats", "Shanghai Hongqiao International Airport", "SHA", "Shanghai", "CN", { "lat": "31.19790077", "lon": "121.3359985" }, "SE-BD", "Rain", 15994.086504249335, 9938.2645998924618, 0, 0, "No Delay", "H3UL18F", 17.771207226943705, 1066.2724336166223, "Mariscal Sucre International Airport", "UIO", "Quito", "EC", { "lat": "-0.129166667", "lon": "-78.3575" }, "SE-BD", "Cloudy", 6, "2018-02-11T19:54:11Z" ], [ 684.9300961506799, 0, "JetBeats", "Vienna International Airport", "VIE", "Vienna", "AT", { "lat": "48.11029816", "lon": "16.56970024" }, "AT-9", "Cloudy", 768.002930319632, 477.21489645447582, 0, 0, "No Delay", "TPA2HJK", 0.64000244193302669, 38.4001465159816, "Ciampino___G. B. Pastine International Airport", "RM12", "Rome", "IT", { "lat": "41.7994", "lon": "12.5949" }, "IT-62", "Heavy Fog", 6, "2018-02-11T19:51:23Z" ], [ 771.28019232022439, 0, "Kibana Airlines", "Cagliari Elmas Airport", "CA07", "Cagliari", "IT", { "lat": "39.251499", "lon": "9.05428" }, "IT-88", "Hail", 16456.262353521666, 10225.447358378113, 0, 0, "No Delay", "DYQ1H18", 13.060525677398148, 783.63154064388891, "Brisbane International Airport", "BNE", "Brisbane", "AU", { "lat": "-27.38419914", "lon": "153.1170044" }, "SE-BD", "Heavy Fog", 6, "2018-02-11T19:40:16Z" ], [ 680.78652721915194, 0, "JetBeats", "Edmonton International Airport", "CYEG", "Edmonton", "CA", { "lat": "53.30970001", "lon": "-113.5800018" }, "CA-AB", "Cloudy", 8953.6337464276367, 5563.5300758741678, 0, 0, "No Delay", "MRGS1S6", 9.3267018191954545, 559.60210915172729, "Ataturk International Airport", "IST", "Istanbul", "TR", { "lat": "40.97689819", "lon": "28.81459999" }, "TR-34", "Rain", 6, "2018-02-11T19:34:39Z" ], [ 1191.0429351122139, 1, "ES-Air", "Munich Airport", "MUC", "Munich", "DE", { "lat": "48.353802", "lon": "11.7861" }, "DE-BY", "Heavy Fog", 8902.9489415275839, 5532.0359982251048, 1, 285, "NAS Delay", "7ZS3Z50", 17.115206863232757, 1026.9124117939655, "Guangzhou Baiyun International Airport", "CAN", "Guangzhou", "CN", { "lat": "23.39240074", "lon": "113.2990036" }, "SE-BD", "Sunny", 6, "2018-02-11T19:21:16Z" ], [ 294.710648297039, 0, "JetBeats", "Treviso-Sant'Angelo Airport", "TV01", "Treviso", "IT", { "lat": "45.648399", "lon": "12.1944" }, "IT-34", "Clear", 0.0, 0.0, 1, 60, "Carrier Delay", "HKKLFK0", 1.0, 60.0, "Treviso-Sant'Angelo Airport", "TV01", "Treviso", "IT", { "lat": "45.648399", "lon": "12.1944" }, "IT-34", "Clear", 6, "2018-02-11T19:17:53Z" ], [ 1191.9641043265337, 0, "Logstash Airways", "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Hail", 5899.45446531149, 3665.751054660464, 1, 360, "Late Aircraft Delay", "ZMOKFBY", 12.145265068032803, 728.71590408196812, "Portland International Jetport Airport", "PWM", "Portland", "US", { "lat": "43.64619827", "lon": "-70.30930328" }, "US-ME", "Clear", 6, "2018-02-11T19:02:10Z" ], [ 863.38806817372051, 0, "Logstash Airways", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Damaging Wind", 2823.4526611229717, 1754.4121462676542, 0, 0, "No Delay", "HSO3VP2", 2.3528772176024764, 141.17263305614858, "Tokyo Haneda International Airport", "HND", "Tokyo", "JP", { "lat": "35.552299", "lon": "139.779999" }, "SE-BD", "Clear", 6, "2018-02-11T18:59:53Z" ], [ 1049.4900036328584, 0, "Logstash Airways", "Portland International Airport", "PDX", "Portland", "US", { "lat": "45.58869934", "lon": "-122.5979996" }, "US-OR", "Damaging Wind", 3617.749726683272, 2247.9654608854735, 1, 300, "Late Aircraft Delay", "LI2IKZC", 8.1734646725291853, 490.40788035175115, "Licenciado Benito Juarez International Airport", "AICM", "Mexico City", "MX", { "lat": "19.4363", "lon": "-99.072098" }, "MX-DIF", "Cloudy", 6, "2018-02-11T18:40:21Z" ], [ 1085.1553392784897, 0, "Logstash Airways", "Melbourne International Airport", "MEL", "Melbourne", "AU", { "lat": "-37.673302", "lon": "144.843002" }, "SE-BD", "Cloudy", 16100.120197134849, 10004.15088205806, 1, 150, "Weather Delay", "O4A214N", 17.407518701050783, 1044.451122063047, "Bologna Guglielmo Marconi Airport", "BO08", "Bologna", "IT", { "lat": "44.5354", "lon": "11.2887" }, "IT-45", "Cloudy", 6, "2018-02-11T18:35:42Z" ], [ 920.60641577916419, 1, "Logstash Airways", "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Sunny", 7446.4285318079565, 4626.99617471961, 0, 0, "No Delay", "OXT7J51", 8.8647958711999486, 531.88775227199687, "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Hail", 6, "2018-02-11T18:28:07Z" ], [ 728.72199250374842, 0, "ES-Air", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Sunny", 12994.258295607915, 8074.2577693817566, 0, 0, "No Delay", "WLGAAS9", 13.535685724591579, 812.14114347549469, "Tampa International Airport", "TPA", "Tampa", "US", { "lat": "27.97550011", "lon": "-82.53320313" }, "US-FL", "Clear", 6, "2018-02-11T18:28:07Z" ], [ 748.91132012619278, 0, "Logstash Airways", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Clear", 10316.155866415009, 6410.16207002046, 0, 0, "No Delay", "RXAWAEN", 10.113878300406871, 606.83269802441225, "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Thunder & Lightning", 6, "2018-02-11T18:24:16Z" ], [ 1190.114570302957, 0, "ES-Air", "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Sunny", 2798.6231896448967, 1738.9838279726998, 0, 0, "No Delay", "368L67X", 3.587978448262688, 215.27870689576128, "Olenya Air Base", "XLMO", "Olenegorsk", "RU", { "lat": "68.15180206", "lon": "33.46390152" }, "RU-MUR", "Sunny", 6, "2018-02-11T18:19:33Z" ], [ 897.39705131952633, 0, "Logstash Airways", "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Sunny", 261.002285347155, 162.17930122283053, 0, 0, "No Delay", "R65C8M6", 0.20714467091044048, 12.428680254626428, "Turin Airport", "TO11", "Torino", "IT", { "lat": "45.200802", "lon": "7.64963" }, "IT-21", "Damaging Wind", 6, "2018-02-11T18:18:07Z" ], [ 1073.8100029687585, 1, "ES-Air", "Verona Villafranca Airport", "VR10", "Verona", "IT", { "lat": "45.395699", "lon": "10.8885" }, "IT-34", "Damaging Wind", 9093.6165223126191, 5650.5113402185107, 1, 195, "Carrier Delay", "HHWEFAG", 17.028206851988816, 1021.692411119329, "New Chitose Airport", "CTS", "Chitose / Tomakomai", "JP", { "lat": "42.77519989", "lon": "141.6920013" }, "SE-BD", "Clear", 6, "2018-02-11T18:13:09Z" ], [ 549.86184823154326, 0, "JetBeats", "Melbourne International Airport", "MEL", "Melbourne", "AU", { "lat": "-37.673302", "lon": "144.843002" }, "SE-BD", "Sunny", 9795.9122912271068, 6086.8976994521408, 0, 0, "No Delay", "FEBSZFG", 13.605433737815426, 816.32602426892561, "Chhatrapati Shivaji International Airport", "BOM", "Mumbai", "IN", { "lat": "19.08869934", "lon": "72.86789703" }, "SE-BD", "Cloudy", 6, "2018-02-11T18:12:20Z" ], [ 231.85512623777851, 0, "ES-Air", "New Chitose Airport", "CTS", "Chitose / Tomakomai", "JP", { "lat": "42.77519989", "lon": "141.6920013" }, "SE-BD", "Damaging Wind", 1039.4592136247936, 645.89001085211964, 0, 0, "No Delay", "MW2DSNT", 1.2374514447914211, 74.247086687485265, "Itami Airport", "ITM", "Osaka", "JP", { "lat": "34.78549957", "lon": "135.4380035" }, "SE-BD", "Rain", 6, "2018-02-11T18:06:11Z" ], [ 868.0747522456586, 1, "ES-Air", "Ukrainka Air Base", "XHBU", "Belogorsk", "RU", { "lat": "51.169997", "lon": "128.445007" }, "RU-AMU", "Heavy Fog", 13450.984211948124, 8358.0540965437613, 1, 180, "Security Delay", "RBZEOFK", 14.799108957849231, 887.9465374709539, "El Dorado International Airport", "BOG", "Bogota", "CO", { "lat": "4.70159", "lon": "-74.1469" }, "SE-BD", "Clear", 6, "2018-02-11T17:37:26Z" ], [ 1019.3545756274339, 0, "ES-Air", "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Thunder & Lightning", 7290.3553136709525, 4530.0167730895018, 1, 45, "NAS Delay", "CEBRVZV", 8.8503947929677249, 531.02368757806346, "Milano Linate Airport", "MI11", "Milan", "IT", { "lat": "45.445099", "lon": "9.27674" }, "IT-25", "Clear", 6, "2018-02-11T17:23:45Z" ], [ 730.44248977898883, 1, "Logstash Airways", "Ministro Pistarini International Airport", "EZE", "Buenos Aires", "AR", { "lat": "-34.8222", "lon": "-58.5358" }, "AR-B", "Sunny", 5947.2054673771754, 3695.4221517445462, 0, 0, "No Delay", "1B1K5HI", 7.0800065087823514, 424.8003905269411, "Luis Munoz Marin International Airport", "SJU", "San Juan", "PR", { "lat": "18.43939972", "lon": "-66.00180054" }, "PR-U-A", "Damaging Wind", 6, "2018-02-11T17:09:05Z" ], [ 684.55612753345918, 0, "Logstash Airways", "Sydney Kingsford Smith International Airport", "SYD", "Sydney", "AU", { "lat": "-33.94609833", "lon": "151.177002" }, "SE-BD", "Hail", 15587.644267855676, 9685.7131028889253, 1, 360, "Security Delay", "K7XY3E3", 29.61764283008436, 1777.0585698050616, "Stockholm-Arlanda Airport", "ARN", "Stockholm", "SE", { "lat": "59.65190125", "lon": "17.91860008" }, "SE-AB", "Sunny", 6, "2018-02-11T17:07:09Z" ], [ 841.89072025689461, 1, "JetBeats", "Treviso-Sant'Angelo Airport", "TV01", "Treviso", "IT", { "lat": "45.648399", "lon": "12.1944" }, "IT-34", "Cloudy", 8185.8556810231057, 5086.45490400008, 0, 0, "No Delay", "XPX46W4", 11.369244001420981, 682.15464008525885, "Tampa International Airport", "TPA", "Tampa", "US", { "lat": "27.97550011", "lon": "-82.53320313" }, "US-FL", "Hail", 6, "2018-02-11T17:04:12Z" ], [ 355.61575230647952, 0, "Kibana Airlines", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Sunny", 0.0, 0.0, 1, 165, "Carrier Delay", "C32CL1J", 2.75, 165.0, "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Hail", 6, "2018-02-11T16:57:01Z" ], [ 1012.6620251058478, 0, "Kibana Airlines", "El Dorado International Airport", "BOG", "Bogota", "CO", { "lat": "4.70159", "lon": "-74.1469" }, "CO-CUN", "Rain", 9746.3269863838959, 6056.0868194642635, 0, 0, "No Delay", "FMJP7A2", 7.7351801479237272, 464.11080887542363, "Bari Karol Wojty__a Airport", "BA02", "Bari", "IT", { "lat": "41.138901", "lon": "16.760599" }, "IT-75", "Rain", 6, "2018-02-11T16:40:54Z" ], [ 925.96759191344518, 0, "JetBeats", "Narita International Airport", "NRT", "Tokyo", "JP", { "lat": "35.76470184", "lon": "140.3860016" }, "SE-BD", "Hail", 9636.2454993724077, 5987.6853546366765, 0, 0, "No Delay", "AG0QLCR", 8.4528469292740418, 507.17081575644249, "London Gatwick Airport", "LGW", "London", "GB", { "lat": "51.14810181", "lon": "-0.190277994" }, "GB-ENG", "Clear", 6, "2018-02-11T16:23:20Z" ], [ 524.39622986737254, 1, "ES-Air", "Turin Airport", "TO11", "Torino", "IT", { "lat": "45.200802", "lon": "7.64963" }, "IT-21", "Damaging Wind", 6220.0271998065373, 3864.9457168924337, 0, 0, "No Delay", "4FAPIOY", 8.6389266663979676, 518.33559998387807, "Bradley International Airport", "BDL", "Hartford", "US", { "lat": "41.93889999", "lon": "-72.68319702" }, "US-CT", "Clear", 6, "2018-02-11T16:16:10Z" ], [ 1122.9557137584175, 0, "Logstash Airways", "Montreal / Pierre Elliott Trudeau International Airport", "YUL", "Montreal", "CA", { "lat": "45.47060013", "lon": "-73.74079895" }, "CA-QC", "Sunny", 7733.56632835579, 4805.415329696938, 1, 30, "Late Aircraft Delay", "Y18SAKV", 10.414828626097167, 624.88971756583, "Ataturk International Airport", "IST", "Istanbul", "TR", { "lat": "40.97689819", "lon": "28.81459999" }, "TR-34", "Rain", 6, "2018-02-11T16:16:10Z" ], [ 538.72090343251773, 0, "ES-Air", "London Luton Airport", "LTN", "London", "GB", { "lat": "51.87469864", "lon": "-0.368333012" }, "GB-ENG", "Sunny", 16505.067926876196, 10255.773735681243, 0, 0, "No Delay", "X2V7XWN", 17.192779090496039, 1031.5667454297623, "Brisbane International Airport", "BNE", "Brisbane", "AU", { "lat": "-27.38419914", "lon": "153.1170044" }, "SE-BD", "Sunny", 6, "2018-02-11T16:09:59Z" ], [ 921.64888827543177, 0, "ES-Air", "Leonardo da Vinci - Fiumicino Airport", "FCO", "Rome", "IT", { "lat": "41.8002778", "lon": "12.2388889" }, "SE-BD", "Rain", 9742.7708213442475, 6053.8771209537845, 0, 0, "No Delay", "NSJQKWC", 14.761773971733708, 885.70643830402253, "Itami Airport", "ITM", "Osaka", "JP", { "lat": "34.78549957", "lon": "135.4380035" }, "SE-BD", "Damaging Wind", 6, "2018-02-11T16:07:20Z" ], [ 1136.6781500052271, 1, "Kibana Airlines", "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Heavy Fog", 2264.78763216076, 1407.2737911601, 1, 345, "Late Aircraft Delay", "62IWL46", 7.736655817684877, 464.19934906109262, "San Antonio International Airport", "SAT", "San Antonio", "US", { "lat": "29.53370094", "lon": "-98.46980286" }, "US-TX", "Cloudy", 6, "2018-02-11T16:03:10Z" ], [ 605.19187610775509, 0, "JetBeats", "Portland International Jetport Airport", "PWM", "Portland", "US", { "lat": "43.64619827", "lon": "-70.30930328" }, "US-ME", "Thunder & Lightning", 11291.997145070151, 7016.52172877281, 0, 0, "No Delay", "SEFB5XW", 9.4099976208917919, 564.59985725350748, "Jeju International Airport", "CJU", "Jeju City", "KR", { "lat": "33.51129913", "lon": "126.4929962" }, "SE-BD", "Cloudy", 6, "2018-02-11T15:55:10Z" ], [ 892.0331086545516, 0, "ES-Air", "London Heathrow Airport", "LHR", "London", "GB", { "lat": "51.4706", "lon": "-0.461941" }, "GB-ENG", "Rain", 347.65282630016048, 216.02145116280948, 0, 0, "No Delay", "0ZEBVTZ", 0.28971068858346705, 17.382641315008023, "Charles de Gaulle International Airport", "CDG", "Paris", "FR", { "lat": "49.01279831", "lon": "2.549999952" }, "FR-J", "Cloudy", 6, "2018-02-11T15:41:06Z" ], [ 529.71654815317549, 0, "ES-Air", "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Rain", 7056.7930552279513, 4384.88791409913, 0, 0, "No Delay", "6G8EGS6", 6.5340676437295846, 392.04405862377507, "Kempegowda International Airport", "BLR", "Bangalore", "IN", { "lat": "13.1979", "lon": "77.706299" }, "SE-BD", "Cloudy", 6, "2018-02-11T15:41:06Z" ], [ 1136.0959442550548, 0, "ES-Air", "Tokyo Haneda International Airport", "HND", "Tokyo", "JP", { "lat": "35.552299", "lon": "139.779999" }, "SE-BD", "Damaging Wind", 10899.138483601675, 6772.41067391538, 0, 0, "No Delay", "MG6614J", 9.5606477926330466, 573.63886755798285, "John F Kennedy International Airport", "JFK", "New York", "US", { "lat": "40.63980103", "lon": "-73.77890015" }, "US-NY", "Thunder & Lightning", 6, "2018-02-11T15:27:27Z" ], [ 480.45632044880244, 0, "ES-Air", "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Clear", 0.0, 0.0, 1, 270, "Late Aircraft Delay", "I9WOJ68", 4.5, 270.0, "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Clear", 6, "2018-02-11T15:27:27Z" ], [ 429.58053887313531, 0, "Logstash Airways", "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Sunny", 0.0, 0.0, 1, 150, "Late Aircraft Delay", "I2IGGNL", 2.5, 150.0, "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Cloudy", 6, "2018-02-11T15:07:11Z" ], [ 858.14433690388387, 0, "JetBeats", "Washington Dulles International Airport", "IAD", "Washington", "US", { "lat": "38.94449997", "lon": "-77.45580292" }, "US-DC", "Heavy Fog", 16809.1419228649, 10444.716557097114, 1, 210, "NAS Delay", "BJ4UMEM", 26.84603044842347, 1610.7618269054083, "Adelaide International Airport", "ADL", "Adelaide", "AU", { "lat": "-34.945", "lon": "138.531006" }, "SE-BD", "Rain", 6, "2018-02-11T14:54:34Z" ], [ 1184.1554070522866, 1, "JetBeats", "Warsaw Chopin Airport", "WAW", "Warsaw", "PL", { "lat": "52.16569901", "lon": "20.96710014" }, "PL-MZ", "Hail", 1246.3042852492376, 774.41757961581709, 0, 0, "No Delay", "JQN64EB", 1.2982336304679558, 77.894017828077352, "Turin Airport", "TO11", "Torino", "IT", { "lat": "45.200802", "lon": "7.64963" }, "IT-21", "Clear", 6, "2018-02-11T14:40:05Z" ], [ 587.04819736377317, 0, "JetBeats", "Ottawa Macdonald-Cartier International Airport", "YOW", "Ottawa", "CA", { "lat": "45.32249832", "lon": "-75.66919708" }, "CA-ON", "Sunny", 6266.5996814090468, 3893.8845153112366, 0, 0, "No Delay", "VAHCDKK", 6.1437251778520068, 368.62351067112041, "Malpensa International Airport", "MI12", "Milan", "IT", { "lat": "45.6306", "lon": "8.72811" }, "IT-25", "Sunny", 6, "2018-02-11T14:39:28Z" ], [ 861.32564969494854, 0, "Logstash Airways", "King Shaka International Airport", "DUR", "Durban", "ZA", { "lat": "-29.61444444", "lon": "31.11972222" }, "SE-BD", "Cloudy", 9221.41433967435, 5729.9212223578979, 0, 0, "No Delay", "RE0BZ6S", 8.0889599470827633, 485.33759682496577, "Cologne Bonn Airport", "CGN", "Cologne", "DE", { "lat": "50.86589813", "lon": "7.142739773" }, "DE-NW", "Cloudy", 6, "2018-02-11T14:32:13Z" ], [ 1007.3040547218907, 0, "Kibana Airlines", "Montreal / Pierre Elliott Trudeau International Airport", "YUL", "Montreal", "CA", { "lat": "45.47060013", "lon": "-73.74079895" }, "CA-QC", "Rain", 4517.5310449555327, 2807.0636513731883, 0, 0, "No Delay", "3CEKFS9", 5.791706467891709, 347.50238807350252, "El Dorado International Airport", "BOG", "Bogota", "CO", { "lat": "4.70159", "lon": "-74.1469" }, "SE-BD", "Clear", 6, "2018-02-11T14:31:39Z" ], [ 928.53109024437617, 1, "ES-Air", "Dublin Airport", "DUB", "Dublin", "IE", { "lat": "53.42129898", "lon": "-6.270070076" }, "IE-D", "Rain", 5942.7694626619987, 3692.6657462059065, 0, 0, "No Delay", "0M0SIGP", 4.9523078855516651, 297.13847313309992, "Al Maktoum International Airport", "DWC", "Jebel Ali", "AE", { "lat": "24.896356", "lon": "55.161389" }, "SE-BD", "Thunder & Lightning", 6, "2018-02-11T14:15:07Z" ], [ 655.38679477159587, 0, "ES-Air", "Rajiv Gandhi International Airport", "HYD", "Hyderabad", "IN", { "lat": "17.23131752", "lon": "78.42985535" }, "SE-BD", "Thunder & Lightning", 15343.038834032279, 9533.7223328463515, 0, 0, "No Delay", "M8D9B52", 15.042194935325764, 902.53169611954581, "Ministro Pistarini International Airport", "EZE", "Buenos Aires", "AR", { "lat": "-34.8222", "lon": "-58.5358" }, "AR-B", "Cloudy", 6, "2018-02-11T14:04:11Z" ], [ 1015.1401424754488, 0, "Kibana Airlines", "Sydney Kingsford Smith International Airport", "SYD", "Sydney", "AU", { "lat": "-33.94609833", "lon": "151.177002" }, "SE-BD", "Damaging Wind", 7900.4759567292895, 4909.12816447527, 0, 0, "No Delay", "KMTEHP9", 6.5837299639410753, 395.0237978364645, "Jeju International Airport", "CJU", "Jeju City", "KR", { "lat": "33.51129913", "lon": "126.4929962" }, "SE-BD", "Sunny", 6, "2018-02-11T14:03:05Z" ], [ 759.34563886354022, 0, "ES-Air", "Turin Airport", "TO11", "Torino", "IT", { "lat": "45.200802", "lon": "7.64963" }, "IT-21", "Cloudy", 6221.0988725051557, 3865.6116234348624, 0, 0, "No Delay", "A2MIKJ8", 7.4060700863156619, 444.3642051789397, "Ottawa Macdonald-Cartier International Airport", "YOW", "Ottawa", "CA", { "lat": "45.32249832", "lon": "-75.66919708" }, "CA-ON", "Heavy Fog", 6, "2018-02-11T14:02:22Z" ], [ 999.02125642634246, 0, "Kibana Airlines", "Kempegowda International Airport", "BLR", "Bangalore", "IN", { "lat": "13.1979", "lon": "77.706299" }, "SE-BD", "Cloudy", 6721.4228198093351, 4176.498511076149, 1, 60, "Security Delay", "B2JWDRX", 8.0014821039680566, 480.08892623808345, "Catania-Fontanarossa Airport", "CT03", "Catania", "IT", { "lat": "37.466801", "lon": "15.0664" }, "IT-82", "Hail", 6, "2018-02-11T13:32:15Z" ], [ 990.38260659260914, 0, "ES-Air", "Narita International Airport", "NRT", "Tokyo", "JP", { "lat": "35.76470184", "lon": "140.3860016" }, "SE-BD", "Rain", 9614.5290987101871, 5974.1914088660888, 1, 300, "Weather Delay", "RX41Z27", 15.015134477823112, 900.90806866938669, "Dublin Airport", "DUB", "Dublin", "IE", { "lat": "53.42129898", "lon": "-6.270070076" }, "IE-D", "Thunder & Lightning", 6, "2018-02-11T13:31:33Z" ], [ 673.70318711783193, 0, "ES-Air", "Helsinki Vantaa Airport", "HEL", "Helsinki", "FI", { "lat": "60.31719971", "lon": "24.9633007" }, "FI-ES", "Sunny", 1900.0150603475022, 1180.6146233170175, 0, 0, "No Delay", "OKRHE5O", 2.4359167440352594, 146.15500464211556, "Charles de Gaulle International Airport", "CDG", "Paris", "FR", { "lat": "49.01279831", "lon": "2.549999952" }, "FR-J", "Sunny", 6, "2018-02-11T13:28:33Z" ], [ 628.88281473295581, 0, "Logstash Airways", "Montreal / Pierre Elliott Trudeau International Airport", "YUL", "Montreal", "CA", { "lat": "45.47060013", "lon": "-73.74079895" }, "CA-QC", "Rain", 12345.581188715831, 7671.1885020951577, 0, 0, "No Delay", "3BKOUKB", 18.705426043508837, 1122.3255626105301, "Guangzhou Baiyun International Airport", "CAN", "Guangzhou", "CN", { "lat": "23.39240074", "lon": "113.2990036" }, "SE-BD", "Thunder & Lightning", 6, "2018-02-11T13:25:53Z" ], [ 1055.3502132890076, 1, "Kibana Airlines", "Warsaw Chopin Airport", "WAW", "Warsaw", "PL", { "lat": "52.16569901", "lon": "20.96710014" }, "PL-MZ", "Sunny", 1246.3042852492376, 774.41757961581709, 0, 0, "No Delay", "OIOFJLD", 1.1539854493048496, 69.239126958290981, "Turin Airport", "TO11", "Torino", "IT", { "lat": "45.200802", "lon": "7.64963" }, "IT-21", "Thunder & Lightning", 6, "2018-02-11T13:20:16Z" ], [ 807.73261475866343, 0, "ES-Air", "Cagliari Elmas Airport", "CA07", "Cagliari", "IT", { "lat": "39.251499", "lon": "9.05428" }, "IT-88", "Sunny", 15564.831100919582, 9671.5376581511355, 1, 285, "Late Aircraft Delay", "M8R7LBZ", 28.333077425635732, 1699.9846455381439, "Adelaide International Airport", "ADL", "Adelaide", "AU", { "lat": "-34.945", "lon": "138.531006" }, "SE-BD", "Cloudy", 6, "2018-02-11T13:10:38Z" ], [ 777.47661678526742, 0, "JetBeats", "Tokyo Haneda International Airport", "HND", "Tokyo", "JP", { "lat": "35.552299", "lon": "139.779999" }, "SE-BD", "Clear", 11692.454203402607, 7265.3542085487043, 0, 0, "No Delay", "QZA8OCM", 14.990325901798213, 899.41955410789285, "Orlando Sanford International Airport", "SFB", "Orlando", "US", { "lat": "28.77759933", "lon": "-81.23750305" }, "US-FL", "Thunder & Lightning", 6, "2018-02-11T12:32:18Z" ], [ 594.0540183976791, 0, "Kibana Airlines", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Rain", 3566.1857360188378, 2215.9250825297995, 1, 315, "Carrier Delay", "2NUO9LB", 9.8220329948959453, 589.3219796937567, "Rajiv Gandhi International Airport", "HYD", "Hyderabad", "IN", { "lat": "17.23131752", "lon": "78.42985535" }, "SE-BD", "Clear", 6, "2018-02-11T12:27:52Z" ], [ 861.38411356370273, 0, "Logstash Airways", "Stockholm-Arlanda Airport", "ARN", "Stockholm", "SE", { "lat": "59.65190125", "lon": "17.91860008" }, "SE-AB", "Sunny", 1466.2887108582138, 911.10956443011173, 1, 255, "Weather Delay", "2V5TSX6", 5.4719072590485114, 328.3144355429107, "London Heathrow Airport", "LHR", "London", "GB", { "lat": "51.4706", "lon": "-0.461941" }, "GB-ENG", "Clear", 6, "2018-02-11T12:24:58Z" ], [ 308.35517140080987, 0, "Logstash Airways", "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Sunny", 0.0, 0.0, 1, 300, "Carrier Delay", "PR96VG3", 5.0, 300.0, "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Clear", 6, "2018-02-11T12:22:16Z" ], [ 994.35627144601767, 0, "ES-Air", "Narita International Airport", "NRT", "Tokyo", "JP", { "lat": "35.76470184", "lon": "140.3860016" }, "SE-BD", "Rain", 11271.561706708899, 7003.8237360743869, 0, 0, "No Delay", "HRXGJGK", 11.050550692851861, 663.03304157111165, "Licenciado Benito Juarez International Airport", "AICM", "Mexico City", "MX", { "lat": "19.4363", "lon": "-99.072098" }, "MX-DIF", "Clear", 6, "2018-02-11T12:13:55Z" ], [ 1175.9079216638825, 0, "ES-Air", "Ukrainka Air Base", "XHBU", "Belogorsk", "RU", { "lat": "51.169997", "lon": "128.445007" }, "RU-AMU", "Rain", 13450.984211948124, 8358.0540965437613, 0, 0, "No Delay", "WT4XS8N", 16.013076442795384, 960.78458656772307, "El Dorado International Airport", "BOG", "Bogota", "CO", { "lat": "4.70159", "lon": "-74.1469" }, "CO-CUN", "Damaging Wind", 6, "2018-02-11T12:07:32Z" ], [ 899.94727743659166, 0, "Kibana Airlines", "Narita International Airport", "NRT", "Tokyo", "JP", { "lat": "35.76470184", "lon": "140.3860016" }, "SE-BD", "Rain", 1296.9342910553842, 805.87760668656551, 1, 330, "Carrier Delay", "LC8WCRT", 6.70086508431054, 402.05190505863243, "Jeju International Airport", "CJU", "Jeju City", "KR", { "lat": "33.51129913", "lon": "126.4929962" }, "SE-BD", "Rain", 6, "2018-02-11T12:05:52Z" ], [ 817.36895217510494, 0, "JetBeats", "Syracuse Hancock International Airport", "SYR", "Syracuse", "US", { "lat": "43.11119843", "lon": "-76.10630035" }, "US-NY", "Rain", 4259.578368081543, 2646.7792890031856, 1, 360, "Security Delay", "VOD2DJ5", 11.916081066779922, 714.96486400679532, "El Dorado International Airport", "BOG", "Bogota", "CO", { "lat": "4.70159", "lon": "-74.1469" }, "CO-CUN", "Thunder & Lightning", 6, "2018-02-11T12:02:49Z" ], [ 530.7993561801253, 0, "Kibana Airlines", "Montreal / Pierre Elliott Trudeau International Airport", "YUL", "Montreal", "CA", { "lat": "45.47060013", "lon": "-73.74079895" }, "CA-QC", "Cloudy", 5261.14701710131, 3269.125194552134, 0, 0, "No Delay", "TQVZA43", 4.6150412430713237, 276.90247458427945, "London Gatwick Airport", "LGW", "London", "GB", { "lat": "51.14810181", "lon": "-0.190277994" }, "GB-ENG", "Rain", 6, "2018-02-11T11:45:58Z" ], [ 603.16832447941374, 0, "Logstash Airways", "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Clear", 11937.503792673972, 7417.6209639915214, 1, 15, "Late Aircraft Delay", "QYKRG4Z", 9.7242093592650569, 583.45256155590346, "Comodoro Arturo Merino Benitez International Airport", "SCL", "Santiago", "CL", { "lat": "-33.39300156", "lon": "-70.78579712" }, "SE-BD", "Clear", 6, "2018-02-11T11:38:46Z" ], [ 1194.0236452922072, 0, "ES-Air", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Rain", 11778.102983821767, 7318.5738933514322, 0, 0, "No Delay", "18ALHUO", 11.547159788060556, 692.82958728363337, "Tulsa International Airport", "TUL", "Tulsa", "US", { "lat": "36.19839859", "lon": "-95.88809967" }, "US-OK", "Hail", 6, "2018-02-11T11:34:41Z" ], [ 852.28318962713251, 0, "ES-Air", "Shanghai Hongqiao International Airport", "SHA", "Shanghai", "CN", { "lat": "31.19790077", "lon": "121.3359985" }, "SE-BD", "Damaging Wind", 11741.628437197684, 7295.9096608293084, 1, 255, "Carrier Delay", "XHBN6H7", 19.303369791279081, 1158.2021874767449, "OR Tambo International Airport", "JNB", "Johannesburg", "ZA", { "lat": "-26.1392", "lon": "28.246" }, "SE-BD", "Sunny", 6, "2018-02-11T11:27:47Z" ], [ 833.04593777381024, 0, "JetBeats", "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Rain", 7136.8453359577352, 4434.6300952175143, 1, 225, "Carrier Delay", "LYSOOZK", 11.184213891622642, 671.05283349735851, "Jorge Chavez International Airport", "LIM", "Lima", "PE", { "lat": "-12.0219", "lon": "-77.114304" }, "SE-BD", "Hail", 6, "2018-02-11T11:24:57Z" ], [ 630.77952610274542, 0, "JetBeats", "Helsinki Vantaa Airport", "HEL", "Helsinki", "FI", { "lat": "60.31719971", "lon": "24.9633007" }, "FI-ES", "Sunny", 6324.57895138743, 3929.9111634227543, 0, 0, "No Delay", "AMNDOW7", 7.5292606564136069, 451.75563938481639, "Beijing Capital International Airport", "PEK", "Beijing", "CN", { "lat": "40.08010101", "lon": "116.5849991" }, "SE-BD", "Cloudy", 6, "2018-02-11T11:23:23Z" ], [ 531.00009263065363, 0, "ES-Air", "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Rain", 291.90172192067166, 181.37932096597845, 1, 30, "Security Delay", "BO1KI4Z", 0.743251434933893, 44.595086096033583, "Verona Villafranca Airport", "VR10", "Verona", "IT", { "lat": "45.395699", "lon": "10.8885" }, "IT-34", "Sunny", 6, "2018-02-11T11:20:18Z" ], [ 357.56284153137523, 1, "Logstash Airways", "Shanghai Pudong International Airport", "PVG", "Shanghai", "CN", { "lat": "31.14340019", "lon": "121.8050003" }, "SE-BD", "Thunder & Lightning", 0.0, 0.0, 0, 0, "No Delay", "UUJQFTA", 0.0, 0.0, "Shanghai Pudong International Airport", "PVG", "Shanghai", "CN", { "lat": "31.14340019", "lon": "121.8050003" }, "SE-BD", "Thunder & Lightning", 6, "2018-02-11T11:19:12Z" ], [ 756.88094073994046, 1, "Kibana Airlines", "Treviso-Sant'Angelo Airport", "TV01", "Treviso", "IT", { "lat": "45.648399", "lon": "12.1944" }, "IT-34", "Heavy Fog", 8700.3509028134686, 5406.14741336437, 0, 0, "No Delay", "425S3GJ", 8.5297557870720286, 511.7853472243217, "Incheon International Airport", "ICN", "Seoul", "KR", { "lat": "37.46910095", "lon": "126.4509964" }, "SE-BD", "Thunder & Lightning", 6, "2018-02-11T11:11:47Z" ], [ 1014.0527874787632, 0, "Logstash Airways", "Vienna International Airport", "VIE", "Vienna", "AT", { "lat": "48.11029816", "lon": "16.56970024" }, "AT-9", "Thunder & Lightning", 6481.1079209495856, 4027.1737558592727, 1, 150, "Security Delay", "QG5GBQP", 11.501538779096647, 690.0923267457988, "Montreal / Pierre Elliott Trudeau International Airport", "YUL", "Montreal", "CA", { "lat": "45.47060013", "lon": "-73.74079895" }, "CA-QC", "Thunder & Lightning", 6, "2018-02-11T10:56:31Z" ], [ 929.69613599614559, 0, "ES-Air", "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Clear", 9595.42771414762, 5962.3223587670618, 0, 0, "No Delay", "0Y830VQ", 9.995237202237103, 599.71423213422622, "Tokyo Haneda International Airport", "HND", "Tokyo", "JP", { "lat": "35.552299", "lon": "139.779999" }, "SE-BD", "Rain", 6, "2018-02-11T10:54:41Z" ], [ 780.86071688336551, 0, "Logstash Airways", "Narita International Airport", "NRT", "Tokyo", "JP", { "lat": "35.76470184", "lon": "140.3860016" }, "SE-BD", "Cloudy", 9733.5659568233386, 6048.157483312044, 0, 0, "No Delay", "YAC758U", 8.1113049640194479, 486.67829784116691, "Charles de Gaulle International Airport", "CDG", "Paris", "FR", { "lat": "49.01279831", "lon": "2.549999952" }, "FR-J", "Clear", 6, "2018-02-11T10:54:28Z" ], [ 1073.1833398797767, 0, "ES-Air", "Stockholm-Arlanda Airport", "ARN", "Stockholm", "SE", { "lat": "59.65190125", "lon": "17.91860008" }, "SE-AB", "Rain", 8547.0806747381539, 5310.9097090107234, 1, 45, "NAS Delay", "YNP23HR", 8.2474391883668012, 494.84635130200809, "San Antonio International Airport", "SAT", "San Antonio", "US", { "lat": "29.53370094", "lon": "-98.46980286" }, "US-TX", "Rain", 6, "2018-02-11T10:41:40Z" ], [ 752.17292470467737, 0, "Logstash Airways", "Shanghai Hongqiao International Airport", "SHA", "Shanghai", "CN", { "lat": "31.19790077", "lon": "121.3359985" }, "SE-BD", "Rain", 9154.0721553726125, 5688.0767290104613, 0, 0, "No Delay", "WK5II29", 7.62839346281051, 457.7036077686306, "Leonardo da Vinci___Fiumicino Airport", "RM11", "Rome", "IT", { "lat": "41.8002778", "lon": "12.2388889" }, "IT-62", "Clear", 6, "2018-02-11T10:41:40Z" ], [ 912.69191284222757, 0, "Kibana Airlines", "Stockholm-Arlanda Airport", "ARN", "Stockholm", "SE", { "lat": "59.65190125", "lon": "17.91860008" }, "SE-AB", "Clear", 8082.1718434091072, 5022.0287542061278, 0, 0, "No Delay", "8AXKQAR", 6.7351432028409226, 404.10859217045538, "Itami Airport", "ITM", "Osaka", "JP", { "lat": "34.78549957", "lon": "135.4380035" }, "SE-BD", "Damaging Wind", 6, "2018-02-11T10:36:27Z" ], [ 376.52968733115756, 0, "Logstash Airways", "New Chitose Airport", "CTS", "Chitose / Tomakomai", "JP", { "lat": "42.77519989", "lon": "141.6920013" }, "SE-BD", "Rain", 786.39909247766855, 488.64574166720632, 0, 0, "No Delay", "J8ENXN0", 1.0922209617745398, 65.533257706472384, "Narita International Airport", "NRT", "Tokyo", "JP", { "lat": "35.76470184", "lon": "140.3860016" }, "SE-BD", "Sunny", 6, "2018-02-11T10:36:27Z" ], [ 825.74385646559858, 0, "JetBeats", "Warsaw Chopin Airport", "WAW", "Warsaw", "PL", { "lat": "52.16569901", "lon": "20.96710014" }, "PL-MZ", "Heavy Fog", 8727.4518136915485, 5422.9871386673994, 1, 345, "Weather Delay", "FWA54GG", 13.022876511409624, 781.3725906845774, "Louis Armstrong New Orleans International Airport", "MSY", "New Orleans", "US", { "lat": "29.99340057", "lon": "-90.25800323" }, "US-LA", "Heavy Fog", 6, "2018-02-11T10:29:03Z" ], [ 1181.8748577991928, 0, "JetBeats", "Tokyo Haneda International Airport", "HND", "Tokyo", "JP", { "lat": "35.552299", "lon": "139.779999" }, "SE-BD", "Clear", 11074.402963368044, 6881.3149726646652, 0, 0, "No Delay", "VQ493AH", 15.381115226900061, 922.86691361400369, "Piedmont Triad International Airport", "GSO", "Greensboro", "US", { "lat": "36.09780121", "lon": "-79.93730164" }, "US-NC", "Rain", 6, "2018-02-11T10:29:03Z" ], [ 729.78817067984289, 1, "ES-Air", "Vienna International Airport", "VIE", "Vienna", "AT", { "lat": "48.11029816", "lon": "16.56970024" }, "AT-9", "Rain", 7240.2829104873626, 4498.9032242251269, 1, 135, "NAS Delay", "FPS5ZUQ", 11.532413987804311, 691.94483926825865, "Ukrainka Air Base", "XHBU", "Belogorsk", "RU", { "lat": "51.169997", "lon": "128.445007" }, "RU-AMU", "Damaging Wind", 6, "2018-02-11T10:24:42Z" ], [ 611.37023245183173, 0, "Logstash Airways", "Jorge Chavez International Airport", "LIM", "Lima", "PE", { "lat": "-12.0219", "lon": "-77.114304" }, "SE-BD", "Sunny", 6777.5106171739053, 4211.3498525945388, 0, 0, "No Delay", "EG26UYG", 5.647925514311587, 338.87553085869524, "Casper-Natrona County International Airport", "CPR", "Casper", "US", { "lat": "42.90800095", "lon": "-106.4639969" }, "US-WY", "Rain", 6, "2018-02-11T10:24:30Z" ], [ 445.71492041764446, 0, "JetBeats", "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Sunny", 0.0, 0.0, 1, 180, "Security Delay", "XUAY4SM", 3.0, 180.0, "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Cloudy", 6, "2018-02-11T10:21:53Z" ], [ 662.30699246014683, 0, "ES-Air", "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Heavy Fog", 10131.225294849231, 6295.2515402855015, 1, 240, "Late Aircraft Delay", "YWT6SEF", 13.932573818479638, 835.9544291087783, "Ministro Pistarini International Airport", "EZE", "Buenos Aires", "AR", { "lat": "-34.8222", "lon": "-58.5358" }, "AR-B", "Sunny", 6, "2018-02-11T10:13:32Z" ], [ 562.34461127886789, 0, "JetBeats", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Rain", 2442.8775632135025, 1517.933743943807, 0, 0, "No Delay", "IYNT0MX", 1.9387917168361133, 116.32750301016679, "Ukrainka Air Base", "XHBU", "Belogorsk", "RU", { "lat": "51.169997", "lon": "128.445007" }, "RU-AMU", "Thunder & Lightning", 6, "2018-02-11T10:05:47Z" ], [ 739.13216536621019, 0, "Logstash Airways", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Rain", 10316.155866415009, 6410.16207002046, 0, 0, "No Delay", "HU0ILVE", 9.04925953194299, 542.9555719165794, "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Hail", 6, "2018-02-11T10:02:21Z" ], [ 802.57533633139542, 0, "Logstash Airways", "Tucson International Airport", "TUS", "Tucson", "US", { "lat": "32.11610031", "lon": "-110.9410019" }, "US-AZ", "Rain", 11077.248476805155, 6883.08309274161, 0, 0, "No Delay", "GNF75KM", 16.783709813341144, 1007.0225888004686, "Shanghai Pudong International Airport", "PVG", "Shanghai", "CN", { "lat": "31.14340019", "lon": "121.8050003" }, "SE-BD", "Sunny", 6, "2018-02-11T09:52:27Z" ], [ 962.34881335032071, 0, "JetBeats", "Ministro Pistarini International Airport", "EZE", "Buenos Aires", "AR", { "lat": "-34.8222", "lon": "-58.5358" }, "SE-BD", "Damaging Wind", 11479.139700433523, 7132.8067215172905, 0, 0, "No Delay", "03OIL3V", 11.254058529836787, 675.24351179020721, "Frankfurt am Main Airport", "FRA", "Frankfurt am Main", "DE", { "lat": "50.033333", "lon": "8.570556" }, "DE-HE", "Cloudy", 6, "2018-02-11T09:16:17Z" ], [ 595.96128497553855, 0, "JetBeats", "Ottawa Macdonald-Cartier International Airport", "YOW", "Ottawa", "CA", { "lat": "45.32249832", "lon": "-75.66919708" }, "CA-ON", "Clear", 6002.0733859587053, 3729.5154957291325, 0, 0, "No Delay", "U2E5Q2L", 6.2521597770403181, 375.12958662241908, "Frankfurt am Main Airport", "FRA", "Frankfurt am Main", "DE", { "lat": "50.033333", "lon": "8.570556" }, "DE-HE", "Clear", 6, "2018-02-11T09:02:07Z" ], [ 685.72618108188817, 0, "ES-Air", "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Sunny", 7426.3681019075239, 4614.5312014755846, 0, 0, "No Delay", "0XG3GDP", 8.8409144070327663, 530.454864421966, "Treviso-Sant'Angelo Airport", "TV01", "Treviso", "IT", { "lat": "45.648399", "lon": "12.1944" }, "IT-34", "Hail", 6, "2018-02-11T08:58:52Z" ], [ 740.02837154345025, 0, "Logstash Airways", "Tokyo Haneda International Airport", "HND", "Tokyo", "JP", { "lat": "35.552299", "lon": "139.779999" }, "SE-BD", "Cloudy", 9371.8977264680834, 5823.4272638218326, 0, 0, "No Delay", "PTFUSLG", 10.413219696075649, 624.79318176453887, "Cologne Bonn Airport", "CGN", "Cologne", "DE", { "lat": "50.86589813", "lon": "7.142739773" }, "DE-NW", "Heavy Fog", 6, "2018-02-11T08:52:06Z" ], [ 623.32247720925147, 0, "JetBeats", "Ukrainka Air Base", "XHBU", "Belogorsk", "RU", { "lat": "51.169997", "lon": "128.445007" }, "RU-AMU", "Sunny", 9343.8857488189551, 5806.0214278730682, 0, 0, "No Delay", "YOQ53TS", 8.6517460637212551, 519.10476382327533, "Portland International Jetport Airport", "PWM", "Portland", "US", { "lat": "43.64619827", "lon": "-70.30930328" }, "US-ME", "Hail", 6, "2018-02-11T08:35:17Z" ], [ 1102.8144645388557, 0, "JetBeats", "Ministro Pistarini International Airport", "EZE", "Buenos Aires", "AR", { "lat": "-34.8222", "lon": "-58.5358" }, "SE-BD", "Hail", 18748.859646655459, 11650.001271732743, 1, 135, "Carrier Delay", "66FQQMD", 28.290082842577025, 1697.4049705546215, "Itami Airport", "ITM", "Osaka", "JP", { "lat": "34.78549957", "lon": "135.4380035" }, "SE-BD", "Hail", 6, "2018-02-11T08:28:21Z" ], [ 1157.5077271483356, 0, "Kibana Airlines", "Turin Airport", "TO11", "Torino", "IT", { "lat": "45.200802", "lon": "7.64963" }, "IT-21", "Heavy Fog", 16681.281547315732, 10365.267803102215, 0, 0, "No Delay", "1L71ZCH", 15.44563106232938, 926.73786373976282, "Sydney Kingsford Smith International Airport", "SYD", "Sydney", "AU", { "lat": "-33.94609833", "lon": "151.177002" }, "SE-BD", "Damaging Wind", 6, "2018-02-11T08:25:54Z" ], [ 697.2083968654315, 0, "Logstash Airways", "Manchester Airport", "MAN", "Manchester", "GB", { "lat": "53.35369873", "lon": "-2.274950027" }, "GB-ENG", "Cloudy", 8806.708392890594, 5472.2348937769639, 0, 0, "No Delay", "6J0PQVN", 7.7251828007812229, 463.51096804687336, "Gimpo International Airport", "GMP", "Seoul", "KR", { "lat": "37.5583", "lon": "126.791" }, "SE-BD", "Hail", 6, "2018-02-11T08:13:59Z" ], [ 1002.2862000309508, 0, "Kibana Airlines", "Warsaw Chopin Airport", "WAW", "Warsaw", "PL", { "lat": "52.16569901", "lon": "20.96710014" }, "PL-MZ", "Clear", 967.5830353995043, 601.22822429480846, 0, 0, "No Delay", "VVEA7OW", 0.89591021796250392, 53.754613077750236, "Treviso-Sant'Angelo Airport", "TV01", "Treviso", "IT", { "lat": "45.648399", "lon": "12.1944" }, "IT-34", "Rain", 6, "2018-02-11T08:06:52Z" ], [ 701.07419678804422, 0, "Logstash Airways", "Jorge Chavez International Airport", "LIM", "Lima", "PE", { "lat": "-12.0219", "lon": "-77.114304" }, "SE-BD", "Cloudy", 9842.94883622534, 6116.1248534964179, 1, 240, "NAS Delay", "5IXRGVO", 16.619165174647872, 997.14991047887224, "Dublin Airport", "DUB", "Dublin", "IE", { "lat": "53.42129898", "lon": "-6.270070076" }, "IE-D", "Clear", 6, "2018-02-11T08:00:22Z" ], [ 1083.6016862775346, 0, "Logstash Airways", "Winnipeg / James Armstrong Richardson International Airport", "YWG", "Winnipeg", "CA", { "lat": "49.90999985", "lon": "-97.23989868" }, "CA-MB", "Sunny", 7446.4285318079565, 4626.99617471961, 0, 0, "No Delay", "P3UKWC7", 6.5319548524631195, 391.91729114778718, "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Cloudy", 6, "2018-02-11T07:51:48Z" ], [ 766.96531716480865, 0, "Kibana Airlines", "Warsaw Chopin Airport", "WAW", "Warsaw", "PL", { "lat": "52.16569901", "lon": "20.96710014" }, "PL-MZ", "Rain", 8142.2197451414922, 5059.34079049693, 0, 0, "No Delay", "Y9U4Y6U", 6.7851831209512437, 407.11098725707461, "Guangzhou Baiyun International Airport", "CAN", "Guangzhou", "CN", { "lat": "23.39240074", "lon": "113.2990036" }, "SE-BD", "Rain", 6, "2018-02-11T07:28:30Z" ], [ 403.6147064445326, 0, "ES-Air", "Tulsa International Airport", "TUL", "Tulsa", "US", { "lat": "36.19839859", "lon": "-95.88809967" }, "US-OK", "Cloudy", 2186.5223553595033, 1358.6420028033181, 0, 0, "No Delay", "59G22E0", 2.6030028039994089, 156.18016823996453, "Spokane International Airport", "GEG", "Spokane", "US", { "lat": "47.61989975", "lon": "-117.5339966" }, "US-WA", "Clear", 6, "2018-02-11T07:14:34Z" ], [ 506.63232728025105, 0, "Kibana Airlines", "Rajiv Gandhi International Airport", "HYD", "Hyderabad", "IN", { "lat": "17.23131752", "lon": "78.42985535" }, "SE-BD", "Rain", 6474.6233011172881, 4023.144399902872, 1, 285, "Late Aircraft Delay", "STIRQ1F", 9.8885899215216568, 593.31539529129941, "Olenya Air Base", "XLMO", "Olenegorsk", "RU", { "lat": "68.15180206", "lon": "33.46390152" }, "RU-MUR", "Sunny", 6, "2018-02-11T07:01:23Z" ], [ 979.92516075176559, 0, "ES-Air", "Sheremetyevo International Airport", "SVO", "Moscow", "RU", { "lat": "55.972599", "lon": "37.4146" }, "RU-MOS", "Clear", 14410.643651074781, 8954.3588263757028, 1, 150, "NAS Delay", "MQTAG6N", 22.514782848714972, 1350.8869709228984, "Melbourne International Airport", "MEL", "Melbourne", "AU", { "lat": "-37.673302", "lon": "144.843002" }, "SE-BD", "Thunder & Lightning", 6, "2018-02-11T06:55:18Z" ], [ 1085.342629338636, 0, "Kibana Airlines", "Olenya Air Base", "XLMO", "Olenegorsk", "RU", { "lat": "68.15180206", "lon": "33.46390152" }, "RU-MUR", "Sunny", 8444.50682235057, 5247.173272060274, 1, 315, "Carrier Delay", "F0Q6RAJ", 11.951989541548072, 717.11937249288428, "Los Angeles International Airport", "LAX", "Los Angeles", "US", { "lat": "33.94250107", "lon": "-118.4079971" }, "US-CA", "Thunder & Lightning", 6, "2018-02-11T06:54:51Z" ], [ 673.41124567239672, 1, "ES-Air", "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Hail", 787.48113960826424, 489.31809458280156, 0, 0, "No Delay", "P5DAUJO", 1.0937238050114782, 65.623428300688687, "Berlin-Tegel Airport", "TXL", "Berlin", "DE", { "lat": "52.5597", "lon": "13.2877" }, "DE-BE", "Rain", 6, "2018-02-11T06:50:34Z" ], [ 326.34911293769522, 0, "Kibana Airlines", "Treviso-Sant'Angelo Airport", "TV01", "Treviso", "IT", { "lat": "45.648399", "lon": "12.1944" }, "IT-34", "Cloudy", 427.56775974975113, 265.67828863794881, 0, 0, "No Delay", "QGD7L5C", 0.59384411076354326, 35.6306466458126, "Leonardo da Vinci - Fiumicino Airport", "FCO", "Rome", "IT", { "lat": "41.8002778", "lon": "12.2388889" }, "SE-BD", "Heavy Fog", 6, "2018-02-11T06:36:31Z" ], [ 841.68625617189434, 0, "Logstash Airways", "Ukrainka Air Base", "XHBU", "Belogorsk", "RU", { "lat": "51.169997", "lon": "128.445007" }, "RU-AMU", "Clear", 6232.2312582294189, 3872.5289672247936, 0, 0, "No Delay", "RT13KQS", 9.4427746336809371, 566.56647802085627, "Stockholm-Arlanda Airport", "ARN", "Stockholm", "SE", { "lat": "59.65190125", "lon": "17.91860008" }, "SE-AB", "Sunny", 6, "2018-02-11T06:31:57Z" ], [ 540.57678193712161, 0, "JetBeats", "Sydney Kingsford Smith International Airport", "SYD", "Sydney", "AU", { "lat": "-33.94609833", "lon": "151.177002" }, "SE-BD", "Rain", 15551.352695623931, 9663.16256538312, 0, 0, "No Delay", "NPLKNZD", 12.95946057968661, 777.5676347811966, "Lester B. Pearson International Airport", "YYZ", "Toronto", "CA", { "lat": "43.67720032", "lon": "-79.63059998" }, "CA-ON", "Clear", 6, "2018-02-11T06:29:14Z" ], [ 988.77060418789245, 0, "ES-Air", "Tulsa International Airport", "TUL", "Tulsa", "US", { "lat": "36.19839859", "lon": "-95.88809967" }, "US-OK", "Clear", 8663.017092577129, 5382.949259187053, 0, 0, "No Delay", "T07V2ZX", 9.023976138101176, 541.43856828607056, "Cagliari Elmas Airport", "CA07", "Cagliari", "IT", { "lat": "39.251499", "lon": "9.05428" }, "IT-88", "Clear", 6, "2018-02-11T06:24:02Z" ], [ 667.58314412471543, 0, "JetBeats", "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Sunny", 9076.9042045365732, 5640.12678739696, 0, 0, "No Delay", "3IJIMJ3", 7.9621966706461169, 477.731800238767, "El Dorado International Airport", "BOG", "Bogota", "CO", { "lat": "4.70159", "lon": "-74.1469" }, "CO-CUN", "Heavy Fog", 6, "2018-02-11T06:22:51Z" ], [ 983.42924366226, 1, "Kibana Airlines", "Venice Marco Polo Airport", "VE05", "Venice", "IT", { "lat": "45.505299", "lon": "12.3519" }, "IT-34", "Thunder & Lightning", 6232.7835257402567, 3872.8721303464372, 1, 270, "NAS Delay", "M6R2H4T", 11.425315028600286, 685.51890171601713, "Chhatrapati Shivaji International Airport", "BOM", "Mumbai", "IN", { "lat": "19.08869934", "lon": "72.86789703" }, "SE-BD", "Thunder & Lightning", 6, "2018-02-11T06:19:58Z" ], [ 1176.1905251500007, 0, "ES-Air", "Il Caravaggio International Airport", "BG01", "Bergamo", "IT", { "lat": "45.673901", "lon": "9.70417" }, "IT-25", "Sunny", 11913.334123112405, 7402.6026276000684, 0, 0, "No Delay", "FY4JKB9", 14.182540622752864, 850.95243736517182, "Comodoro Arturo Merino Benitez International Airport", "SCL", "Santiago", "CL", { "lat": "-33.39300156", "lon": "-70.78579712" }, "SE-BD", "Clear", 6, "2018-02-11T06:17:54Z" ], [ 479.3173585143557, 0, "ES-Air", "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Sunny", 0.0, 0.0, 1, 330, "Carrier Delay", "RWJDQQH", 5.5, 330.0, "Xi'an Xianyang International Airport", "XIY", "Xi'an", "CN", { "lat": "34.447102", "lon": "108.751999" }, "SE-BD", "Rain", 6, "2018-02-11T06:04:44Z" ], [ 1181.7700219583969, 0, "Kibana Airlines", "Ottawa Macdonald-Cartier International Airport", "YOW", "Ottawa", "CA", { "lat": "45.32249832", "lon": "-75.66919708" }, "CA-ON", "Cloudy", 5648.0124842383129, 3509.5122511025065, 0, 0, "No Delay", "0S0S03D", 4.7066770701985945, 282.40062421191567, "Amsterdam Airport Schiphol", "AMS", "Amsterdam", "NL", { "lat": "52.30860138", "lon": "4.76388979" }, "NL-NH", "Hail", 6, "2018-02-11T06:01:48Z" ], [ 947.59293297909494, 0, "Kibana Airlines", "Zurich Airport", "ZRH", "Zurich", "CH", { "lat": "47.464699", "lon": "8.54917" }, "CH-ZH", "Clear", 9001.0705118200513, 5593.0059153419352, 0, 0, "No Delay", "O0CZ9AW", 9.37611511647922, 562.5669069887532, "New Chitose Airport", "CTS", "Chitose / Tomakomai", "JP", { "lat": "42.77519989", "lon": "141.6920013" }, "SE-BD", "Heavy Fog", 6, "2018-02-11T05:55:58Z" ], [ 514.788416546782, 0, "JetBeats", "Mariscal Sucre International Airport", "UIO", "Quito", "EC", { "lat": "-0.129166667", "lon": "-78.3575" }, "SE-BD", "Heavy Fog", 14714.628391255823, 9143.2461868039536, 0, 0, "No Delay", "O0UFN3A", 16.349587101395358, 980.97522608372151, "Chubu Centrair International Airport", "NGO", "Tokoname", "JP", { "lat": "34.85839844", "lon": "136.8049927" }, "SE-BD", "Sunny", 6, "2018-02-11T05:43:48Z" ], [ 942.893674824945, 0, "Logstash Airways", "Luis Munoz Marin International Airport", "SJU", "San Juan", "PR", { "lat": "18.43939972", "lon": "-66.00180054" }, "PR-U-A", "Cloudy", 13663.716346166211, 8490.2397164100475, 1, 285, "NAS Delay", "EJSEIOZ", 16.735716093128257, 1004.1429655876954, "Gimpo International Airport", "GMP", "Seoul", "KR", { "lat": "37.5583", "lon": "126.791" }, "SE-BD", "Sunny", 6, "2018-02-11T05:43:23Z" ] ]azure-kusto-python-3.0.1/azure-kusto-ingest/tests/input/dataset.csv000066400000000000000000000030411417222763000255500ustar00rootroot000000000000000,00000000-0000-0000-0001-020304050607,0,0,0,0,0,0,0,0,0,0,2014-01-01T01:01:01.0000000Z,Zero,"Zero",0,00:00:00,,null 1,00000001-0000-0000-0001-020304050607,1.0001,1.01,1,1,1,1,1,1,1,1,2015-01-01T01:01:01.0000000Z,One,"One",1,00:00:01.0010001,,"{""rowId"": 1, ""arr"": [0,1]}" 2,00000002-0000-0000-0001-020304050607,2.0002,2.02,0,2,2,2,2,2,2,2,2016-01-01T01:01:01.0000000Z,Two,"Two",2,-00:00:02.0020002,,"{""rowId"": 2, ""arr"": [0,2]}" 3,00000003-0000-0000-0001-020304050607,3.0003,3.03,1,3,3,3,3,3,3,3,2017-01-01T01:01:01.0000000Z,Three,"Three",3,00:00:03.0030003,,"{""rowId"": 3, ""arr"": [0,3]}" 4,00000004-0000-0000-0001-020304050607,4.0004,4.04,0,4,4,4,4,4,4,4,2018-01-01T01:01:01.0000000Z,Four,"Four",4,-00:00:04.0040004,,"{""rowId"": 4, ""arr"": [0,4]}" 5,00000005-0000-0000-0001-020304050607,5.0005,5.05,1,5,5,5,5,5,5,5,2019-01-01T01:01:01.0000000Z,Five,"Five",5,00:00:05.0050005,,"{""rowId"": 5, ""arr"": [0,5]}" 6,00000006-0000-0000-0001-020304050607,6.0006,6.06,0,6,6,6,6,6,6,6,2020-01-01T01:01:01.0000000Z,Six,"Six",6,-00:00:06.0060006,,"{""rowId"": 6, ""arr"": [0,6]}" 7,00000007-0000-0000-0001-020304050607,7.0007,7.07,1,7,7,7,7,7,7,7,2021-01-01T01:01:01.0000000Z,Seven,"Seven",7,00:00:07.0070007,,"{""rowId"": 7, ""arr"": [0,7]}" 8,00000008-0000-0000-0001-020304050607,8.0008,8.08,0,8,8,8,8,8,8,8,2022-01-01T01:01:01.0000000Z,Eight,"Eight",8,-00:00:08.0080008,,"{""rowId"": 8, ""arr"": [0,8]}" 9,00000009-0000-0000-0001-020304050607,9.0009,9.09,1,9,9,9,9,9,9,9,2023-01-01T01:01:01.0000000Z,Nine,"Nine",9,00:00:09.0090009,,"{""rowId"": 9, ""arr"": [0,9]}"azure-kusto-python-3.0.1/azure-kusto-ingest/tests/input/dataset.csv.gz000066400000000000000000000007151417222763000261740ustar00rootroot00000000000000‹ó@Zÿdataset.csv}ÔËj[A à}ž"h-—¹_¼O!›vѬRº(ôЂ ‡æ¥ï^ýƒµPéŒeFÏùõ!°»ëë`pÁE—\vÅUvÿ©à|:à¢pþ8Þ®Yü¸í&œ„Ëî8ÞÌç—çç¯CýrèˆóhrüS2oLr{oýˆcúM´_Þîo=ß}ßw|þêØûC7AyaÉ Ð¶aËù2å=¼Éjä ¹x¸úFžõë ðEõÅ¥/bbD‹²‡hJ|uî{Ú7YàhÄQWˆ¼8"-1Zb1)1-‰ Cš<Àɔ۔øñò²3á$¹ªKD\‰V˜¬0A˜U˜—ÂŒ™-˳)ö¹ðô*;ÄIœu…HË#гf‹ËX0² É\L¹9~9½3ÉArQˆ°2ò¬¯X_¯ª¯.}G“4WSâósßöºEˆF\u…È«#Ò«%V›Û’Ø0´¡ÉÜL 1L‰w§ŸO¿˜F#¹¬kD`™Öج±ÁØÕØ—ÆŽ©­Ë»)1Æ©ñÓ †8‰».i}Z`·À.À¿Jzå!azure-kusto-python-3.0.1/azure-kusto-ingest/tests/input/dataset.csv.zip000066400000000000000000000021631417222763000263550ustar00rootroot00000000000000PKÕf.PJzå²! dataset.csv}ÔËJ,1à½O!µ®irOgöç€]èJq!Øè€Ì@ã Äw·þ¶`2Õ¤¦úÏG-F±Z?»âÐ;e”UNyTdõG¥ÝƒúFéýò kÖ-ßNó‰ 'aXí—‡ùøöòr¦ó¥º{é§¥ÉQ.÷Í˯Ž“$ƒË½¿¿ê%Žé‹h>}\<íÏ5Ÿ=Ì3¾ß)Ö÷ßtf2ÏtyfÀšÁ6¶^hòn>d5r îV ò*Ÿ)}>›}¶ë³FЬìÁn ¾Øö=Ï“,pi$Ã+y‘Ñ–D ¢ËD×%º#hò»m86‰ÿOo3N’ѼDÄ9$VBW „> }W茠yY¢ß„©-<¼Ëq’Œ®@¤yV@_=€!CŒ É Š22Ù^>™ä Ì DX@^å ¥/À³/v}Q"[”ÆmÁ§Û¾é}:2-dx%"/"²"Æ’A3qìÇ#hòÛÑ4‰ÿOϯLK#ÎkDàˆÌÊ8–ÆÆ”©kLFÐ’¬1m FÛ4^ðgˆ“dt"-!°¦˜øPKÕf.PJzå¯!more_csv/dataset2.csv}ÔËj[A à}ž"h-—¹_¼O!›vѬRº(ôЂ ‡æ¥ï^ýƒµPéŒeFÏùõ!°»ëë`pÁE—\vÅUvÿ©à|:à¢pþ8Þ®Yü¸í&œ„Ëî8ÞÌç—çç¯CýrèˆóhrüS2oLr{oýˆcúM´_Þîo=ß}ßw|þêØûC7AyaÉ Ð¶aËù2å=¼Éjä ¹x¸úFžõë ðEõÅ¥/bbD‹²‡hJ|uî{Ú7YàhÄQWˆ¼8"-1Zb1)1-‰ Cš<Àɔ۔øñò²3á$¹ªKD\‰V˜¬0A˜U˜—ÂŒ™-˳)ö¹ðô*;ÄIœu…HË#гf‹ËX0² É\L¹9~9½3ÉArQˆ°2ò¬¯X_¯ª¯.}G“4WSâósßöºEˆF\u…È«#Ò«%V›Û’Ø0´¡ÉÜL 1L‰w§ŸO¿˜F#¹¬kD`™Öج±ÁØÕØ—ÆŽ©­Ë»)1Æ©ñÓ †8‰».i}Z`·À.À¿PKÕf.PJzå²! $ dataset.csv ukh¤ÀÊÕukh¤ÀÊÕukh¤ÀÊÕPKÕf.PJzå¯! Ûmore_csv/dataset2.csvPK ½azure-kusto-python-3.0.1/azure-kusto-ingest/tests/input/dataset.json000066400000000000000000000014071417222763000257320ustar00rootroot00000000000000{"rownumber": 0, "rowguid": "00000000-0000-0000-0001-020304050607", "xdouble": 0.0, "xfloat": 0.0, "xbool": 0, "xint16": 0, "xint32": 0, "xint64": 0, "xunit8": 0, "xuint16": 0, "xunit32": 0, "xunit64": 0, "xdate": "2014-01-01T01:01:01Z", "xsmalltext": "Zero", "xtext": "Zero", "xnumberAsText": "0", "xtime": "00:00:00", "xtextWithNulls": null, "xdynamicWithNulls": ""} {"rownumber": 1, "rowguid": "00000001-0000-0000-0001-020304050607", "xdouble": 1.00001, "xfloat": 1.01, "xbool": 1, "xint16": 1, "xint32": 1, "xint64": 1, "xuint8": 1, "xuint16": 1, "xuint32": 1, "xuint64": 1, "xdate": "2015-01-01T01:01:01Z", "xsmalltext": "One", "xtext": "One", "xnumberAsText": "1", "xtime": "00:00:01.0010001", "xtextWithNulls": null, "xdynamicWithNulls": "{\"rowId\":1,\"arr\":[0,1]}"}azure-kusto-python-3.0.1/azure-kusto-ingest/tests/input/dataset.jsonz.gz000066400000000000000000000004641417222763000265450ustar00rootroot00000000000000‹4íµVdataset.json_•Akƒ0Çïƒ}‡’³–÷¬uÅÛŽ»l— sÙˆ hÂÅï>mLÖÂV êï½üÃËïHZù%tSÑ–ä+ˆV†?4«G"0?qðÂØ@ [ÈàŽŒ‘¾–ºâÔ°Ãï\–jÁJJ>ŸÞ3¡0ó`“x¥'Ђ©ƒ)´´lj¡%V—ŠšÉÀ46“â0·ë`'íš’sE{evh+mõ¬0¹ïös¦m¬¡“—Ü.—}fêóQsÞ]1~í(ߢlØ›ß"d¸½ ãEãø㸶g}*¡§}íèkG_;žLï!¤´‹ýÒ¾ý[û“ ¶ò¹t¼$ÝÜͯ–,Œé‡º 9F)Ûvü{_2ü+}á(azure-kusto-python-3.0.1/azure-kusto-ingest/tests/input/dataset.tsv000066400000000000000000000030421417222763000255720ustar00rootroot000000000000000 00000000-0000-0000-0001-020304050607 0 0 0 0 0 0 0 0 0 0 2014-01-01T01:01:01.0000000Z Zero "Zero" 0 00:00:00 null 1 00000001-0000-0000-0001-020304050607 1.0001 1.01 1 1 1 1 1 1 1 1 2015-01-01T01:01:01.0000000Z One "One" 1 00:00:01.0010001 "{""rowId"": 1, ""arr"": [0,1]}" 2 00000002-0000-0000-0001-020304050607 2.0002 2.02 0 22 2 2 2 2 2 2 2016-01-01T01:01:01.0000000Z Two "Two" 2 -00:00:02.0020002 "{""rowId"": 2, ""arr"": [0,2]}" 3 00000003-0000-0000-0001-020304050607 3.0003 3.03 1 3 3 3 3 3 3 3 2017-01-01T01:01:01.0000000Z Three "Three" 3 00:00:03.0030003 "{""rowId"": 3, ""arr"": [0,3]}" 4 00000004-0000-0000-0001-020304050607 4.0004 4.04 0 4 4 4 4 4 4 4 2018-01-01T01:01:01.0000000Z Four "Four" 4 -00:00:04.0040004 "{""rowId"": 4, ""arr"": [0,4]}" 5 00000005-0000-0000-0001-020304050607 5.0005 5.05 1 5 5 5 5 5 5 5 2019-01-01T01:01:01.0000000Z Five "Five" 5 00:00:05.0050005 "{""rowId"": 5, ""arr"": [0,5]}" 6 00000006-0000-0000-0001-020304050607 6.0006 6.06 0 6 6 6 6 6 6 6 2020-01-01T01:01:01.0000000Z Six "Six" 6 -00:00:06.0060006 "{""rowId"": 6, ""arr"": [0,6]}" 7 00000007-0000-0000-0001-020304050607 7.0007 7.07 1 7 7 7 7 7 7 7 2021-01-01T01:01:01.0000000Z Seven "Seven" 7 00:00:07.0070007 "{""rowId"": 7, ""arr"": [0,7]}" 8 00000008-0000-0000-0001-020304050607 8.0008 8.08 0 8 8 8 8 8 8 8 2022-01-01T01:01:01.0000000Z Eight "Eight" 8 -00:00:08.0080008 "{""rowId"": 8, ""arr"": [0,8]}" 9 00000009-0000-0000-0001-020304050607 9.0009 9.09 1 9 9 9 9 9 9 9 2023-01-01T01:01:01.0000000Z Nine "Nine" 9 00:00:09.0090009 "{""rowId"": 9, ""arr"": [0,9]}"azure-kusto-python-3.0.1/azure-kusto-ingest/tests/sample.py000066400000000000000000000227341417222763000241140ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import io from azure.kusto.data import KustoConnectionStringBuilder from azure.kusto.data.data_format import DataFormat from azure.kusto.ingest import ( QueuedIngestClient, IngestionProperties, FileDescriptor, BlobDescriptor, StreamDescriptor, KustoStreamingIngestClient, ManagedStreamingIngestClient, IngestionStatus, ) ################################################################## ## AUTH ## ################################################################## cluster = "https://ingest-{cluster_name}.kusto.windows.net/" # In case you want to authenticate with AAD application. client_id = "" client_secret = "" # read more at https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id authority_id = "" kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(cluster, client_id, client_secret, authority_id) # In case you want to authenticate with AAD application certificate. filename = "path to a PEM certificate" with open(filename, "r") as pem_file: PEM = pem_file.read() thumbprint = "certificate's thumbprint" kcsb = KustoConnectionStringBuilder.with_aad_application_certificate_authentication(cluster, client_id, PEM, thumbprint, authority_id) # In case you want to authenticate with AAD application certificate Subject Name & Issuer filename = "path to a PEM certificate" with open(filename, "r") as pem_file: PEM = pem_file.read() filename = "path to a public certificate" with open(filename, "r") as cert_file: public_certificate = cert_file.read() thumbprint = "certificate's thumbprint" kcsb = KustoConnectionStringBuilder.with_aad_application_certificate_sni_authentication(cluster, client_id, PEM, public_certificate, thumbprint, authority_id) # In case you want to authenticate with a System Assigned Managed Service Identity (MSI) kcsb = KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(cluster) # In case you want to authenticate with a User Assigned Managed Service Identity (MSI) user_assigned_client_id = "the AAD identity client id" kcsb = KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(cluster, client_id=user_assigned_client_id) # In case you want to authenticate with AAD username and password username = "" password = "" kcsb = KustoConnectionStringBuilder.with_aad_user_password_authentication(cluster, username, password, authority_id) # In case you want to authenticate with AAD device code. # Please note that if you choose this option, you'll need to autenticate for every new instance that is initialized. # It is highly recommended to create one instance and use it for all of your queries. kcsb = KustoConnectionStringBuilder.with_aad_device_authentication(cluster) # The authentication method will be taken from the chosen KustoConnectionStringBuilder. client = QueuedIngestClient(kcsb) # there are more options for authenticating - see azure-kusto-data samples ################################################################## ## INGESTION ## ################################################################## # there are a lot of useful properties, make sure to go over docs and check them out ingestion_props = IngestionProperties( database="{database_name}", table="{table_name}", data_format=DataFormat.CSV, # in case status update for success are also required # report_level=ReportLevel.FailuresAndSuccesses, # in case a mapping is required # ingestion_mapping_reference="{json_mapping_that_already_exists_on_table}" # ingestion_mapping_type=IngestionMappingType.JSON ) # ingest from file file_descriptor = FileDescriptor("{filename}.csv", 3333) # 3333 is the raw size of the data in bytes. client.ingest_from_file(file_descriptor, ingestion_properties=ingestion_props) result = client.ingest_from_file("{filename}.csv", ingestion_properties=ingestion_props) # Inspect the result for useful information, such as source_id and blob_url print(repr(result)) # ingest from blob blob_descriptor = BlobDescriptor( "https://{path_to_blob}.csv.gz?sp=rl&st=2020-05-20T13:38:37Z&se=2020-05-21T13:38:37Z&sv=2019-10-10&sr=c&sig=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 10, ) # 10 is the raw size of the data in bytes. client.ingest_from_blob(blob_descriptor, ingestion_properties=ingestion_props) # ingest from dataframe import pandas fields = ["id", "name", "value"] rows = [[1, "abc", 15.3], [2, "cde", 99.9]] df = pandas.DataFrame(data=rows, columns=fields) client.ingest_from_dataframe(df, ingestion_properties=ingestion_props) # ingest a whole folder. import os path = "folder/path" [client.ingest_from_file(f, ingestion_properties=ingestion_props) for f in os.listdir(path)] ################################################################## ## INGESTION STATUS ## ################################################################## # if status updates are required, something like this can be done import pprint import time from azure.kusto.ingest.status import KustoIngestStatusQueues qs = KustoIngestStatusQueues(client) MAX_BACKOFF = 180 backoff = 1 while True: ################### NOTICE #################### # in order to get success status updates, # make sure ingestion properties set the # reportLevel=ReportLevel.FailuresAndSuccesses. if qs.success.is_empty() and qs.failure.is_empty(): time.sleep(backoff) backoff = min(backoff * 2, MAX_BACKOFF) print("No new messages. backing off for {} seconds".format(backoff)) continue backoff = 1 success_messages = qs.success.pop(10) failure_messages = qs.failure.pop(10) pprint.pprint("SUCCESS : {}".format(success_messages)) pprint.pprint("FAILURE : {}".format(failure_messages)) # you can of course separate them and dump them into a file for follow up investigations with open("successes.log", "w+") as sf: for sm in success_messages: sf.write(str(sm)) with open("failures.log", "w+") as ff: for fm in failure_messages: ff.write(str(fm)) ################################################################## ## STREAMING INGEST ## ################################################################## # Authenticate against this cluster endpoint as shows in the Auth section cluster = "https://{cluster_name}.kusto.windows.net" client = KustoStreamingIngestClient(kcsb) ingestion_properties = IngestionProperties(database="{database_name}", table="{table_name}", data_format=DataFormat.CSV) # ingest from file file_descriptor = FileDescriptor("{filename}.csv", 3333) # 3333 is the raw size of the data in bytes. client.ingest_from_file(file_descriptor, ingestion_properties=ingestion_properties) client.ingest_from_file("{filename}.csv", ingestion_properties=ingestion_properties) # ingest from dataframe import pandas fields = ["id", "name", "value"] rows = [[1, "abc", 15.3], [2, "cde", 99.9]] df = pandas.DataFrame(data=rows, columns=fields) client.ingest_from_dataframe(df, ingestion_properties=ingestion_properties) # ingest from stream byte_sequence = b"56,56,56" bytes_stream = io.BytesIO(byte_sequence) client.ingest_from_stream(bytes_stream, ingestion_properties=ingestion_properties) stream_descriptor = StreamDescriptor(bytes_stream) client.ingest_from_stream(stream_descriptor, ingestion_properties=ingestion_properties) str_sequence = u"57,57,57" str_stream = io.StringIO(str_sequence) client.ingest_from_stream(str_stream, ingestion_properties=ingestion_properties) ################################################################## ## NANAGED STREAMING INGEST ## ################################################################## # Managed streaming ingest client will try to use streaming ingestion for performance, but will fall back to queued ingestion if unable. dm_cluster = "https://ingest-{cluster_name}.kusto.windows.net" kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(dm_cluster, client_id, client_secret, authority_id) # Create it from a dm connection string client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb) # or an engine connection string, like a streaming ingestion client with `from_engine_kcsb` # or provide both: `ManagedStreamingIngestClient(engine_kcsb, dm_kcsb)` # use client as you would a streaming or queued ingestion client byte_sequence = b"56,56,56" bytes_stream = io.BytesIO(byte_sequence) client.ingest_from_stream(bytes_stream, ingestion_properties=ingestion_properties) ingestion_properties = IngestionProperties(database="{database_name}", table="{table_name}", data_format=DataFormat.CSV) # ingest from file file_descriptor = FileDescriptor("{filename}.csv", 3333) # 3333 is the raw size of the data in bytes. result = client.ingest_from_file(file_descriptor, ingestion_properties=ingestion_properties) # inspect the result to see what type of ingestion was preformed if result.status == IngestionStatus.QUEUED: # fell back to queued ingestion pass # Managed streaming ingest client will fall back to queued if: # - Multiple transient errors were encountered when trying to do streaming ingestion # - The ingestion is too large for streaming ingestion (over 4MB) # - The ingestion is directly for a blob azure-kusto-python-3.0.1/azure-kusto-ingest/tests/test_connection_string.py000066400000000000000000000031311417222763000274050ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import unittest from azure.kusto.ingest._resource_manager import _ResourceUri class ResourceUriTests(unittest.TestCase): """Tests class connection_string.""" def test_blob_uri(self): """Tests parsing blob uris.""" storage_name = "storageaccountname" container_name = "containername" endpoint_suffix = "core.windows.net" container_sas = "somesas" uri = "https://{}.blob.{}/{}?{}".format(storage_name, endpoint_suffix, container_name, container_sas) connection_string = _ResourceUri.parse(uri) assert connection_string.storage_account_name == storage_name assert connection_string.object_type == "blob" assert connection_string.endpoint_suffix == endpoint_suffix assert connection_string.sas == container_sas assert connection_string.object_name == container_name def test_queue_uri(self): """Tests parsing queues uris.""" storage_name = "storageaccountname" queue_name = "queuename" endpoint_suffix = "core.windows.net" queue_sas = "somesas" uri = "https://{}.queue.{}/{}?{}".format(storage_name, endpoint_suffix, queue_name, queue_sas) connection_string = _ResourceUri.parse(uri) assert connection_string.storage_account_name == storage_name assert connection_string.object_type == "queue" assert connection_string.endpoint_suffix == endpoint_suffix assert connection_string.sas == queue_sas assert connection_string.object_name == queue_name azure-kusto-python-3.0.1/azure-kusto-ingest/tests/test_descriptors.py000066400000000000000000000146721417222763000262350ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import sys import uuid from io import BytesIO from os import path import pytest from azure.kusto.ingest import FileDescriptor, BlobDescriptor, StreamDescriptor class TestDescriptors: """Test class for FileDescriptor and BlobDescriptor.""" # this is the size with LF line endings uncompressed_size = 1569 # this is the size with CRLF line endings uncompressed_size_2 = 1578 mock_size = 10 INVALID_UUID = "12345" TEST_UUID_STR = "5bcc12b7-e35c-4c76-a40a-2d89e6c2c7dd" TEST_UUID = uuid.UUID("5bcc12b7-e35c-4c76-a40a-2d89e6c2c7dd", version=4) def test_unzipped_file_with_size(self): """Tests FileDescriptor with size and unzipped file.""" filePath = path.join(path.dirname(path.abspath(__file__)), "input", "dataset.csv") descriptor = FileDescriptor(filePath, self.mock_size) with descriptor.open(True) as stream: assert descriptor.size == self.mock_size assert descriptor.stream_name.endswith(".csv.gz") if sys.version_info[0] >= 3: assert stream.readable() assert stream.tell() == 0 assert stream.closed is True def test_unzipped_file_without_size(self): """Tests FileDescriptor without size and unzipped file.""" filePath = path.join(path.dirname(path.abspath(__file__)), "input", "dataset.csv") descriptor = FileDescriptor(filePath, 0) with descriptor.open(True) as stream: # TODO: since we don't know if the file is opened on CRLF system or an LF system, allow both sizes # a more robust approach would be to open the file and check assert descriptor.size in (self.uncompressed_size, self.uncompressed_size_2) assert descriptor.stream_name.endswith(".csv.gz") if sys.version_info[0] >= 3: assert stream.readable() assert stream.tell() == 0 assert stream.closed is True def test_zipped_file_with_size(self): """Tests FileDescriptor with size and zipped file.""" filePath = path.join(path.dirname(path.abspath(__file__)), "input", "dataset.csv.gz") descriptor = FileDescriptor(filePath, self.mock_size) with descriptor.open(False) as stream: assert descriptor.size == self.mock_size assert descriptor.stream_name.endswith(".csv.gz") if sys.version_info[0] >= 3: assert stream.readable() assert stream.tell() == 0 assert stream.closed is True def test_gzip_file_without_size(self): """Tests FileDescriptor without size and zipped file.""" filePath = path.join(path.dirname(path.abspath(__file__)), "input", "dataset.csv.gz") descriptor = FileDescriptor(filePath, 0) with descriptor.open(False) as stream: assert descriptor.size == self.uncompressed_size assert descriptor.stream_name.endswith(".csv.gz") if sys.version_info[0] >= 3: assert stream.readable() assert stream.tell() == 0 assert stream.closed is True def test_zip_file_without_size(self): """Tests FileDescriptor without size and zipped file.""" filePath = path.join(path.dirname(path.abspath(__file__)), "input", "dataset.csv.zip") descriptor = FileDescriptor(filePath, 0) with descriptor.open(False) as stream: # the zip archive contains 2 copies of the source file assert descriptor.size == self.uncompressed_size * 2 assert descriptor.stream_name.endswith(".csv.zip") if sys.version_info[0] >= 3: assert stream.readable() assert stream.tell() == 0 assert stream.closed is True def test_unzipped_file_dont_compress(self): """Tests FileDescriptor with size and unzipped file.""" filePath = path.join(path.dirname(path.abspath(__file__)), "input", "dataset.csv") descriptor = FileDescriptor(filePath, self.mock_size) with descriptor.open(False) as stream: assert descriptor.size == self.mock_size assert descriptor.stream_name.endswith(".csv") if sys.version_info[0] >= 3: assert stream.readable() assert stream.tell() == 0 assert stream.closed is True def test_uuid_stream_descriptor(self): dummy_stream = BytesIO(b"dummy") descriptor = StreamDescriptor(dummy_stream) assert descriptor.source_id assert descriptor.source_id != TestDescriptors.TEST_UUID assert uuid.UUID(str(descriptor.source_id), version=4) descriptor = StreamDescriptor(dummy_stream, source_id=TestDescriptors.TEST_UUID_STR) assert descriptor.source_id == TestDescriptors.TEST_UUID descriptor = StreamDescriptor(dummy_stream, source_id=TestDescriptors.TEST_UUID) assert descriptor.source_id == TestDescriptors.TEST_UUID with pytest.raises(ValueError): StreamDescriptor(dummy_stream, source_id=TestDescriptors.INVALID_UUID) def test_uuid_file_descriptor(self): dummy_file = "dummy" descriptor = FileDescriptor(dummy_file) assert descriptor.source_id assert descriptor.source_id != TestDescriptors.TEST_UUID assert uuid.UUID(str(descriptor.source_id), version=4) descriptor = FileDescriptor(dummy_file, source_id=TestDescriptors.TEST_UUID_STR) assert descriptor.source_id == TestDescriptors.TEST_UUID descriptor = FileDescriptor(dummy_file, source_id=TestDescriptors.TEST_UUID) assert descriptor.source_id == TestDescriptors.TEST_UUID with pytest.raises(ValueError): FileDescriptor(dummy_file, source_id=TestDescriptors.INVALID_UUID) def test_uuid_blob_descriptor(self): dummy_file = "dummy" descriptor = BlobDescriptor(dummy_file) assert descriptor.source_id assert descriptor.source_id != TestDescriptors.TEST_UUID assert uuid.UUID(str(descriptor.source_id), version=4) descriptor = BlobDescriptor(dummy_file, source_id=TestDescriptors.TEST_UUID_STR) assert descriptor.source_id == TestDescriptors.TEST_UUID descriptor = BlobDescriptor(dummy_file, source_id=TestDescriptors.TEST_UUID) assert descriptor.source_id == TestDescriptors.TEST_UUID with pytest.raises(ValueError): BlobDescriptor(dummy_file, source_id=TestDescriptors.INVALID_UUID) azure-kusto-python-3.0.1/azure-kusto-ingest/tests/test_ingestion_blob_info.py000066400000000000000000000143411417222763000276750ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import json import re import unittest from uuid import UUID from azure.kusto.data.data_format import DataFormat from azure.kusto.ingest import ( BlobDescriptor, IngestionProperties, ColumnMapping, ReportLevel, ReportMethod, ValidationPolicy, ValidationOptions, ValidationImplications, ) from azure.kusto.ingest.ingestion_blob_info import IngestionBlobInfo TIMESTAMP_REGEX = "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6}" class IngestionBlobInfoTest(unittest.TestCase): """Tests serialization of ingestion blob info. This serialization will be queued to the DM.""" def test_blob_info_csv_mapping(self): """Tests serialization of csv ingestion blob info.""" validation_policy = ValidationPolicy(ValidationOptions.ValidateCsvInputConstantColumns, ValidationImplications.BestEffort) column_mapping = ColumnMapping("ColumnName", "cslDataType", ordinal=1) properties = IngestionProperties( database="database", table="table", data_format=DataFormat.CSV, column_mappings=[column_mapping], additional_tags=["tag"], ingest_if_not_exists=["ingestIfNotExistTags"], ingest_by_tags=["ingestByTags"], drop_by_tags=["dropByTags"], flush_immediately=True, report_level=ReportLevel.DoNotReport, report_method=ReportMethod.Queue, validation_policy=validation_policy, ) blob = BlobDescriptor("somepath", 10) blob_info = IngestionBlobInfo(blob, properties, auth_context="authorizationContextText") self._verify_ingestion_blob_info_result(blob_info.to_json()) def test_blob_csv_mapping_reference(self): """Tests serialization of ingestion blob info with csv mapping reference.""" validation_policy = ValidationPolicy(ValidationOptions.ValidateCsvInputConstantColumns, ValidationImplications.BestEffort) properties = IngestionProperties( database="database", table="table", data_format=DataFormat.CSV, ingestion_mapping_reference="csvMappingReference", additional_tags=["tag"], ingest_if_not_exists=["ingestIfNotExistTags"], ingest_by_tags=["ingestByTags"], drop_by_tags=["dropByTags"], flush_immediately=True, report_level=ReportLevel.DoNotReport, report_method=ReportMethod.Queue, validation_policy=validation_policy, ) blob = BlobDescriptor("somepath", 10) blob_info = IngestionBlobInfo(blob, properties, auth_context="authorizationContextText") self._verify_ingestion_blob_info_result(blob_info.to_json()) def test_blob_info_json_mapping(self): """Tests serialization of json ingestion blob info.""" validation_policy = ValidationPolicy(ValidationOptions.ValidateCsvInputConstantColumns, ValidationImplications.BestEffort) properties = IngestionProperties( database="database", table="table", data_format=DataFormat.JSON, column_mappings=[ColumnMapping("ColumnName", "datatype", path="jsonpath")], additional_tags=["tag"], ingest_if_not_exists=["ingestIfNotExistTags"], ingest_by_tags=["ingestByTags"], drop_by_tags=["dropByTags"], flush_immediately=True, report_level=ReportLevel.DoNotReport, report_method=ReportMethod.Queue, validation_policy=validation_policy, ) blob = BlobDescriptor("somepath", 10) blob_info = IngestionBlobInfo(blob, properties, auth_context="authorizationContextText") self._verify_ingestion_blob_info_result(blob_info.to_json()) def test_blob_json_mapping_reference(self): """Tests serialization of ingestion blob info with json mapping reference.""" validation_policy = ValidationPolicy(ValidationOptions.ValidateCsvInputConstantColumns, ValidationImplications.BestEffort) properties = IngestionProperties( database="database", table="table", data_format=DataFormat.JSON, ingestion_mapping_reference="jsonMappingReference", additional_tags=["tag"], ingest_if_not_exists=["ingestIfNotExistTags"], ingest_by_tags=["ingestByTags"], drop_by_tags=["dropByTags"], flush_immediately=True, report_level=ReportLevel.DoNotReport, report_method=ReportMethod.Queue, validation_policy=validation_policy, ) blob = BlobDescriptor("somepath", 10) blob_info = IngestionBlobInfo(blob, properties, auth_context="authorizationContextText") self._verify_ingestion_blob_info_result(blob_info.to_json()) def _verify_ingestion_blob_info_result(self, ingestion_blob_info): result = json.loads(ingestion_blob_info) assert result is not None assert isinstance(result, dict) assert result["BlobPath"] == "somepath" assert result["DatabaseName"] == "database" assert result["TableName"] == "table" assert isinstance(result["RawDataSize"], int) assert isinstance(result["IgnoreSizeLimit"], bool) assert isinstance(result["FlushImmediately"], bool) assert isinstance(result["RetainBlobOnSuccess"], bool) assert isinstance(result["ReportMethod"], int) assert isinstance(result["ReportLevel"], int) assert isinstance(UUID(result["Id"]), UUID) assert re.match(TIMESTAMP_REGEX, result["SourceMessageCreationTime"]) assert result["AdditionalProperties"]["authorizationContext"] == "authorizationContextText" assert result["AdditionalProperties"]["ingestIfNotExists"] == '["ingestIfNotExistTags"]' assert result["AdditionalProperties"]["ValidationPolicy"] in ( '{"ValidationImplications":1,"ValidationOptions":1}', '{"ValidationImplications":ValidationImplications.BestEffort,"ValidationOptions":ValidationOptions.ValidateCsvInputConstantColumns}', ) assert result["AdditionalProperties"]["tags"] == '["tag","drop-by:dropByTags","ingest-by:ingestByTags"]' azure-kusto-python-3.0.1/azure-kusto-ingest/tests/test_ingestion_properties.py000066400000000000000000000055511417222763000301430ustar00rootroot00000000000000import pytest from azure.kusto.data.data_format import IngestionMappingKind, DataFormat from azure.kusto.ingest import IngestionProperties, ColumnMapping, TransformationMethod from azure.kusto.ingest.exceptions import KustoDuplicateMappingError, KustoMissingMappingError, KustoMappingError def test_duplicate_reference_and_column_mappings_raises(): """Tests invalid ingestion properties.""" with pytest.raises(KustoDuplicateMappingError): IngestionProperties( database="database", table="table", column_mappings=[ColumnMapping("test", "int")], ingestion_mapping_reference="ingestionMappingReference" ) def test_mapping_kind_without_mapping_raises(): with pytest.raises(KustoMissingMappingError): IngestionProperties(database="database", table="table", ingestion_mapping_kind=IngestionMappingKind.CSV) def test_mapping_kind_data_format_mismatch(): with pytest.raises(KustoMappingError): IngestionProperties( database="database", table="table", ingestion_mapping_reference="ingestionMappingReference", data_format=DataFormat.JSON, ingestion_mapping_kind=IngestionMappingKind.CSV, ) def test_mapping_kind_data_format_invalid_no_name(): with pytest.raises(KustoMappingError): IngestionProperties( database="database", table="table", column_mappings=[ColumnMapping("", "int")], data_format=DataFormat.JSON, ingestion_mapping_kind=IngestionMappingKind.JSON, ) def test_mapping_kind_data_format_invalid_no_path(): with pytest.raises(KustoMappingError): IngestionProperties( database="database", table="table", column_mappings=[ColumnMapping("test", "int")], data_format=DataFormat.JSON, ingestion_mapping_kind=IngestionMappingKind.JSON, ) def test_mapping_kind_data_format_with_path(): IngestionProperties( database="database", table="table", column_mappings=[ColumnMapping("test", "int", "path")], data_format=DataFormat.JSON, ingestion_mapping_kind=IngestionMappingKind.JSON, ) def test_mapping_kind_data_format_with_transform(): IngestionProperties( database="database", table="table", column_mappings=[ColumnMapping("test", "int", transform=TransformationMethod.SOURCE_LINE_NUMBER)], data_format=DataFormat.JSON, ingestion_mapping_kind=IngestionMappingKind.JSON, ) def test_mapping_kind_data_format_with_no_columns(): with pytest.raises(KustoMappingError): IngestionProperties( database="database", table="table", column_mappings=[ColumnMapping("test", "int")], data_format=DataFormat.AVRO, ingestion_mapping_kind=IngestionMappingKind.AVRO, ) azure-kusto-python-3.0.1/azure-kusto-ingest/tests/test_kusto_ingest_client.py000066400000000000000000000377341417222763000277540ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import io import json import os import uuid from pathlib import Path import pytest import responses from mock import patch from azure.kusto.data.data_format import DataFormat from azure.kusto.ingest import QueuedIngestClient, IngestionProperties, IngestionStatus from azure.kusto.ingest.exceptions import KustoInvalidEndpointError from azure.kusto.ingest.managed_streaming_ingest_client import ManagedStreamingIngestClient pandas_installed = False try: import pandas pandas_installed = True except: pass UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" BLOB_NAME_REGEX = "database__table__" + UUID_REGEX + "__dataset.csv.gz" BLOB_URL_REGEX = "https://storageaccount.blob.core.windows.net/tempstorage/database__table__" + UUID_REGEX + "__dataset.csv.gz[?]sas" STORAGE_QUEUE_URL = "https://storageaccount.queue.core.windows.net/readyforaggregation-secured?sp=rl&st=2020-05-20T13:38:37Z&se=2020-05-21T13:38:37Z&sv=2019-10-10&sr=c&sig=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" TEMP_STORAGE_URL = "https://storageaccount.blob.core.windows.net/tempstorage?sp=rl&st=2020-05-20T13:38:37Z&se=2020-05-21T13:38:37Z&sv=2019-10-10&sr=c&sig=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" def request_callback(request): body = json.loads(request.body.decode()) if type(request.body) == bytes else json.loads(request.body) response_status = 400 response_headers = dict() response_body = {} if ".get ingestion resources" in body["csl"]: response_status = 200 response_body = { "Tables": [ { "TableName": "Table_0", "Columns": [{"ColumnName": "ResourceTypeName", "DataType": "String"}, {"ColumnName": "StorageRoot", "DataType": "String"}], "Rows": [ [ "SecuredReadyForAggregationQueue", STORAGE_QUEUE_URL, ], [ "SecuredReadyForAggregationQueue", STORAGE_QUEUE_URL, ], [ "SecuredReadyForAggregationQueue", STORAGE_QUEUE_URL, ], [ "SecuredReadyForAggregationQueue", STORAGE_QUEUE_URL, ], [ "SecuredReadyForAggregationQueue", STORAGE_QUEUE_URL, ], [ "FailedIngestionsQueue", "https://storageaccount.queue.core.windows.net/failedingestions?sp=rl&st=2020-05-20T13:38:37Z&se=2020-05-21T13:38:37Z&sv=2019-10-10&sr=c&sig=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", ], [ "SuccessfulIngestionsQueue", "https://storageaccount.queue.core.windows.net/successfulingestions?sp=rl&st=2020-05-20T13:38:37Z&se=2020-05-21T13:38:37Z&sv=2019-10-10&sr=c&sig=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", ], [ "TempStorage", TEMP_STORAGE_URL, ], [ "TempStorage", TEMP_STORAGE_URL, ], [ "TempStorage", TEMP_STORAGE_URL, ], [ "TempStorage", TEMP_STORAGE_URL, ], [ "TempStorage", TEMP_STORAGE_URL, ], [ "IngestionsStatusTable", "https://storageaccount.table.core.windows.net/ingestionsstatus?sp=rl&st=2020-05-20T13:38:37Z&se=2020-05-21T13:38:37Z&sv=2019-10-10&sr=c&sig=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", ], ], } ] } if ".get kusto identity token" in body["csl"]: response_status = 200 response_body = { "Tables": [{"TableName": "Table_0", "Columns": [{"ColumnName": "AuthorizationContext", "DataType": "String"}], "Rows": [["authorization_context"]]}] } return response_status, response_headers, json.dumps(response_body) def request_error_callback(request): body = json.loads(request.body.decode()) if type(request.body) == bytes else json.loads(request.body) response_status = 400 response_headers = dict() response_body = {} if ".get ingestion resources" in body["csl"]: response_status = 400 response_body = { "error": { "code": "BadRequest", "message": "Request is invalid and cannot be executed.", "@type": "Kusto.Common.Svc.Exceptions.AdminCommandWrongEndpointException", "@message": "Cannot get ingestion resources from this service endpoint. The appropriate endpoint is most likely " "'https://ingest-somecluster.kusto.windows.net/'.", "@context": { "timestamp": "2021-10-12T06:05:35.6602087Z", "serviceAlias": "SomeCluster", "machineName": "KEngine000000", "processName": "Kusto.WinSvc.Svc", "processId": 2648, "threadId": 472, "appDomainName": "Kusto.WinSvc.Svc.exe", "clientRequestId": "KPC.execute;a3dfb878-9d2b-49d6-89a5-e9b3a9f1f674", "activityId": "87eb8fc9-78b3-4580-bcc8-6c90482f9118", "subActivityId": "bbfb038b-4467-4f96-afd4-945904fc6278", "activityType": "DN.AdminCommand.IngestionResourcesGetCommand", "parentActivityId": "00e678e9-4204-4143-8c94-6afd94c27430", "activityStack": "(Activity stack: CRID=KPC.execute;a3dfb878-9d2b-49d6-89a5-e9b3a9f1f674 ARID=87eb8fc9-78b3-4580-bcc8-6c90482f9118 > DN.Admin.Client.ExecuteControlCommand/833dfb85-5d67-44b7-882d-eb2283e65780 > P.WCF.Service.ExecuteControlCommand..IInterNodeCommunicationAdminContract/3784e74f-1d89-4c15-adef-0a360c4c431e > DN.FE.ExecuteControlCommand/00e678e9-4204-4143-8c94-6afd94c27430 > DN.AdminCommand.IngestionResourcesGetCommand/bbfb038b-4467-4f96-afd4-945904fc6278)", }, "@permanent": True, } } if ".show version" in body["csl"]: response_status = 200 response_body = { "Tables": [ { "TableName": "Table_0", "Columns": [ {"ColumnName": "BuildVersion", "DataType": "String"}, {"ColumnName": "BuildTime", "DataType": "DateTime"}, {"ColumnName": "ServiceType", "DataType": "String"}, {"ColumnName": "ProductVersion", "DataType": "String"}, ], "Rows": [["1.0.0.0", "2000-01-01T00:00:00Z", "Engine", "2020-09-07 12-09-22"]], } ] } return response_status, response_headers, json.dumps(response_body) def assert_queued_upload(mock_put_message_in_queue, mock_upload_blob_from_stream, expected_url: str, check_raw_data: bool = True, format: str = "csv"): # mock_put_message_in_queue assert mock_put_message_in_queue.call_count == 1 put_message_in_queue_mock_kwargs = mock_put_message_in_queue.call_args_list[0][1] queued_message_json = json.loads(put_message_in_queue_mock_kwargs["content"]) # mock_upload_blob_from_stream # not checking the query string because it can change order, just checking it's there assert queued_message_json["BlobPath"].startswith(expected_url) is True assert len(queued_message_json["BlobPath"]) > len(expected_url) assert queued_message_json["DatabaseName"] == "database" assert queued_message_json["IgnoreSizeLimit"] is False assert queued_message_json["AdditionalProperties"]["format"] == format assert queued_message_json["FlushImmediately"] is False assert queued_message_json["TableName"] == "table" if check_raw_data: assert queued_message_json["RawDataSize"] > 0 assert queued_message_json["RetainBlobOnSuccess"] is True if mock_upload_blob_from_stream is not None: upload_blob_kwargs = mock_upload_blob_from_stream.call_args_list[0][1] assert issubclass(type(upload_blob_kwargs["data"]), io.BufferedIOBase) @pytest.fixture(params=[QueuedIngestClient, ManagedStreamingIngestClient]) def ingest_client_class(request): if request.param == ManagedStreamingIngestClient: return ManagedStreamingIngestClient.from_dm_kcsb return request.param class TestQueuedIngestClient: MOCKED_UUID_4 = uuid.UUID("11111111-1111-1111-1111-111111111111") MOCKED_PID = 64 MOCKED_TIME = 100 @responses.activate @patch( "azure.kusto.ingest.managed_streaming_ingest_client.ManagedStreamingIngestClient.MAX_STREAMING_SIZE_IN_BYTES", new=0 ) # Trick to always fallback to queued ingest @patch("azure.kusto.data.security._AadHelper.acquire_authorization_header", return_value=None) @patch("azure.storage.blob.BlobClient.upload_blob") @patch("azure.storage.queue.QueueClient.send_message") @patch("uuid.uuid4", return_value=MOCKED_UUID_4) def test_sanity_ingest_from_file(self, mock_uuid, mock_put_message_in_queue, mock_upload_blob_from_stream, mock_aad, ingest_client_class): responses.add_callback( responses.POST, "https://ingest-somecluster.kusto.windows.net/v1/rest/mgmt", callback=request_callback, content_type="application/json" ) ingest_client = ingest_client_class("https://ingest-somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.CSV) # ensure test can work when executed from within directories current_dir = os.getcwd() path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.csv"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) result = ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.QUEUED assert_queued_upload( mock_put_message_in_queue, mock_upload_blob_from_stream, "https://storageaccount.blob.core.windows.net/tempstorage/database__table__11111111-1111-1111-1111-111111111111__dataset.csv.gz?", ) @responses.activate @patch("azure.kusto.ingest.managed_streaming_ingest_client.ManagedStreamingIngestClient.MAX_STREAMING_SIZE_IN_BYTES", new=0) def test_ingest_from_file_wrong_endpoint(self, ingest_client_class): responses.add_callback( responses.POST, "https://somecluster.kusto.windows.net/v1/rest/mgmt", callback=request_error_callback, content_type="application/json" ) ingest_client = ingest_client_class("https://somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.CSV) current_dir = os.getcwd() path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.csv"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) with pytest.raises(KustoInvalidEndpointError) as ex: ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert ( ex.value.args[0] == "You are using 'DataManagement' client type, but the provided endpoint is of ServiceType 'Engine'. Initialize the " "client with the appropriate endpoint URI: 'https://ingest-somecluster.kusto.windows.net'" ), ("Expected exception was " "not raised") @responses.activate @pytest.mark.skipif(not pandas_installed, reason="requires pandas") @patch("azure.kusto.ingest.managed_streaming_ingest_client.ManagedStreamingIngestClient.MAX_STREAMING_SIZE_IN_BYTES", new=0) @patch("azure.storage.blob.BlobClient.upload_blob") @patch("azure.storage.queue.QueueClient.send_message") @patch("uuid.uuid4", return_value=MOCKED_UUID_4) @patch("time.time", return_value=MOCKED_TIME) @patch("os.getpid", return_value=MOCKED_PID) def test_simple_ingest_from_dataframe(self, mock_pid, mock_time, mock_uuid, mock_put_message_in_queue, mock_upload_blob_from_stream, ingest_client_class): responses.add_callback( responses.POST, "https://ingest-somecluster.kusto.windows.net/v1/rest/mgmt", callback=request_callback, content_type="application/json" ) ingest_client = ingest_client_class("https://ingest-somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.CSV) from pandas import DataFrame fields = ["id", "name", "value"] rows = [[1, "abc", 15.3], [2, "cde", 99.9]] df = DataFrame(data=rows, columns=fields) result = ingest_client.ingest_from_dataframe(df, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.QUEUED expected_url = "https://storageaccount.blob.core.windows.net/tempstorage/database__table__11111111-1111-1111-1111-111111111111__df_{0}_100_11111111-1111-1111-1111-111111111111.csv.gz?".format( id(df) ) assert_queued_upload(mock_put_message_in_queue, mock_upload_blob_from_stream, expected_url) @responses.activate @patch("azure.kusto.ingest.managed_streaming_ingest_client.ManagedStreamingIngestClient.MAX_STREAMING_SIZE_IN_BYTES", new=0) @patch("azure.kusto.data.security._AadHelper.acquire_authorization_header", return_value=None) @patch("azure.storage.blob.BlobClient.upload_blob") @patch("azure.storage.queue.QueueClient.send_message") @patch("uuid.uuid4", return_value=MOCKED_UUID_4) def test_sanity_ingest_from_stream(self, mock_uuid, mock_put_message_in_queue, mock_upload_blob_from_stream, mock_aad, ingest_client_class): responses.add_callback( responses.POST, "https://ingest-somecluster.kusto.windows.net/v1/rest/mgmt", callback=request_callback, content_type="application/json" ) ingest_client = ingest_client_class("https://ingest-somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.CSV) # ensure test can work when executed from within directories current_dir = os.getcwd() path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.csv"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) result = ingest_client.ingest_from_stream(io.StringIO(Path(file_path).read_text()), ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.QUEUED assert_queued_upload( mock_put_message_in_queue, mock_upload_blob_from_stream, "https://storageaccount.blob.core.windows.net/tempstorage/database__table__11111111-1111-1111-1111-111111111111__stream.gz?", check_raw_data=False, ) azure-kusto-python-3.0.1/azure-kusto-ingest/tests/test_kusto_streaming_ingest_client.py000066400000000000000000000204131417222763000320070ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import io import json import os import uuid import pytest import responses from azure.kusto.data.data_format import DataFormat from azure.kusto.ingest import KustoStreamingIngestClient, IngestionProperties, IngestionStatus, ManagedStreamingIngestClient from azure.kusto.ingest.exceptions import KustoMissingMappingError pandas_installed = False try: import pandas pandas_installed = True except: pass UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" BLOB_NAME_REGEX = "database__table__" + UUID_REGEX + "__dataset.csv.gz" BLOB_URL_REGEX = "https://storageaccount.blob.core.windows.net/tempstorage/database__table__" + UUID_REGEX + "__dataset.csv.gz[?]sas" def request_callback(request, client_type, custom_request_id=None): request_id = request.headers["x-ms-client-request-id"] if custom_request_id: assert custom_request_id == request_id elif client_type == KustoStreamingIngestClient: [prefix, request_uuid] = request_id.split(";") assert prefix == "KPC.executeStreamingIngest" uuid.UUID(request_uuid) elif client_type == ManagedStreamingIngestClient: assert_managed_streaming_request_id(request_id) response_status = 200 response_headers = dict() response_body = { "Tables": [ { "TableName": "Table_0", "Columns": [ {"ColumnName": "ConsumedRecordsCount", "DataType": "Int64"}, {"ColumnName": "UpdatePolicyStatus", "DataType": "String"}, {"ColumnName": "UpdatePolicyFailureCode", "DataType": "String"}, {"ColumnName": "UpdatePolicyFailureReason", "DataType": "String"}, ], "Rows": [[0, "Inactive", "Unknown", None]], } ] } return response_status, response_headers, json.dumps(response_body) def assert_managed_streaming_request_id(request_id: str, retry: int = 0): [prefix, request_uuid, suffix] = request_id.split(";") assert prefix == "KPC.executeManagedStreamingIngest" uuid.UUID(request_uuid) assert int(suffix) == retry @pytest.fixture(params=[KustoStreamingIngestClient, ManagedStreamingIngestClient]) def ingest_client_class(request): if request.param == ManagedStreamingIngestClient: return ManagedStreamingIngestClient.from_engine_kcsb return request.param class TestKustoStreamingIngestClient: @responses.activate def test_streaming_ingest_from_file(self, ingest_client_class): responses.add_callback( responses.POST, "https://somecluster.kusto.windows.net/v1/rest/ingest/database/table", callback=lambda r: request_callback(r, ingest_client_class) ) ingest_client = ingest_client_class("https://somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.CSV) # ensure test can work when executed from within directories current_dir = os.getcwd() path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.csv"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) result = ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.csv.gz"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) result = ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.JSON, ingestion_mapping_reference="JsonMapping") path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.json"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) result = ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.jsonz.gz"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) result = ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.TSV) path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.tsv"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) result = ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS @pytest.mark.skipif(not pandas_installed, reason="requires pandas") @responses.activate def test_streaming_ingest_from_dataframe(self, ingest_client_class): responses.add_callback( responses.POST, "https://somecluster.kusto.windows.net/v1/rest/ingest/database/table", callback=lambda r: request_callback(r, ingest_client_class) ) ingest_client = ingest_client_class("https://somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.CSV) from pandas import DataFrame fields = ["id", "name", "value"] rows = [[1, "abc", 15.3], [2, "cde", 99.9]] df = DataFrame(data=rows, columns=fields) result = ingest_client.ingest_from_dataframe(df, ingestion_properties) assert result.status == IngestionStatus.SUCCESS @responses.activate def test_streaming_ingest_from_stream(self, ingest_client_class): responses.add_callback( responses.POST, "https://somecluster.kusto.windows.net/v1/rest/ingest/database/table", callback=lambda r: request_callback(r, ingest_client_class) ) ingest_client = ingest_client_class("https://somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.CSV) byte_sequence = b"56,56,56" bytes_stream = io.BytesIO(byte_sequence) result = ingest_client.ingest_from_stream(bytes_stream, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS str_sequence = u"57,57,57" str_stream = io.StringIO(str_sequence) result = ingest_client.ingest_from_stream(str_stream, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS byte_sequence = b'{"Name":"Ben","Age":"56","Weight":"75"}' bytes_stream = io.BytesIO(byte_sequence) with pytest.raises(KustoMissingMappingError): ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.JSON) ingestion_properties.ingestion_mapping_reference = "JsonMapping" result = ingest_client.ingest_from_stream(bytes_stream, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS str_sequence = u'{"Name":"Ben","Age":"56","Weight":"75"}' str_stream = io.StringIO(str_sequence) result = ingest_client.ingest_from_stream(str_stream, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS azure-kusto-python-3.0.1/azure-kusto-ingest/tests/test_managed_streaming_ingest.py000066400000000000000000000404611417222763000307050ustar00rootroot00000000000000import io import json import os import uuid from tempfile import NamedTemporaryFile import pytest import responses from mock import patch from azure.kusto.data.data_format import DataFormat from azure.kusto.data.exceptions import KustoApiError from azure.kusto.ingest import ManagedStreamingIngestClient, IngestionProperties, IngestionStatus, BlobDescriptor from azure.kusto.ingest._retry import ExponentialRetry from test_kusto_ingest_client import request_callback as queued_request_callback, assert_queued_upload from test_kusto_streaming_ingest_client import request_callback as streaming_request_callback, assert_managed_streaming_request_id def mock_retry(self): self.max_attempts = ManagedStreamingIngestClient.ATTEMPT_COUNT self.sleep_base_secs = 0 self.max_jitter_secs = 0 self.current_attempt = 0 class TransientResponseHelper: def __init__(self, times_to_fail): self.times_to_fail = times_to_fail self.total_calls = 0 def transient_error_callback(helper: TransientResponseHelper, request, custom_request_id=None): if custom_request_id: assert request.headers["x-ms-client-request-id"] == custom_request_id else: assert_managed_streaming_request_id(request.headers["x-ms-client-request-id"], helper.total_calls) response_headers = dict() helper.total_calls += 1 if helper.total_calls <= helper.times_to_fail: response_status = 400 response_body = { "error": { "code": "General_InternalServerError", "message": "Unknown error", "@type": "Kusto.DataNode.Exceptions.InternalServerError", "@message": "InternalServerError", "@context": { "timestamp": "2021-10-21T14:12:00.7878847Z", "serviceAlias": "SomeCluster", "machineName": "KEngine000000", "processName": "Kusto.WinSvc.Svc", "processId": 3040, "threadId": 7524, "appDomainName": "Kusto.WinSvc.Svc.exe", "clientRequestId": "KNC.executeStreamingIngest;a4d80ca5-8729-404b-b745-0d5555737483", "activityId": "bace3d9d-d949-457e-b741-1d2c6bc56658", "subActivityId": "bace3d9d-d949-457e-b741-1d2c6bc56658", "activityType": "PO.OWIN.CallContext", "parentActivityId": "bace3d9d-d949-457e-b741-1d2c6bc56658", "activityStack": "(Activity stack: CRID=KNC.executeStreamingIngest;a4d80ca5-8729-404b-b745-0d5555737493 ARID=bace3d9d-d949-457e-b741-1d2c6bc56678 > PO.OWIN.CallContext/bace3d9d-d949-457e-b741-1d2c6bc56678 > PO.OWIN.CallContext/bace3d9d-d949-457e-b741-1d2c6bc56678)", }, "@permanent": False, } } else: response_status = 200 response_body = { "Tables": [ { "TableName": "Table_0", "Columns": [ {"ColumnName": "ConsumedRecordsCount", "DataType": "Int64"}, {"ColumnName": "UpdatePolicyStatus", "DataType": "String"}, {"ColumnName": "UpdatePolicyFailureCode", "DataType": "String"}, {"ColumnName": "UpdatePolicyFailureReason", "DataType": "String"}, ], "Rows": [[0, "Inactive", "Unknown", None]], } ] } return response_status, response_headers, json.dumps(response_body) class TestManagedStreamingIngestClient: MOCKED_UUID_4 = uuid.UUID("11111111-1111-1111-1111-111111111111") @responses.activate @patch("azure.kusto.data.security._AadHelper.acquire_authorization_header", return_value=None) @patch("azure.storage.blob.BlobClient.upload_blob") @patch("azure.storage.queue.QueueClient.send_message") @patch("uuid.uuid4", return_value=MOCKED_UUID_4) def test_fallback_big_file(self, mock_uuid, mock_put_message_in_queue, mock_upload_blob_from_stream, mock_aad): responses.add_callback( responses.POST, "https://ingest-somecluster.kusto.windows.net/v1/rest/mgmt", callback=queued_request_callback, content_type="application/json" ) responses.add_callback( responses.POST, "https://somecluster.kusto.windows.net/v1/rest/ingest/database/table", callback=streaming_request_callback, content_type="application/json", ) data_format = DataFormat.ORC # Using orc to avoid compression ingest_client = ManagedStreamingIngestClient.from_dm_kcsb("https://ingest-somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=data_format) initial_bytes = bytearray(os.urandom(5 * 1024 * 1024)) def check_bytes(data): assert data.read() == initial_bytes mock_upload_blob_from_stream.side_effect = check_bytes f = NamedTemporaryFile(dir=".", mode="wb", delete=False) try: f.write(initial_bytes) f.close() result = ingest_client.ingest_from_file(f.name, ingestion_properties=ingestion_properties) finally: os.unlink(f.name) assert result.status == IngestionStatus.QUEUED assert_queued_upload( mock_put_message_in_queue, mock_upload_blob_from_stream, "https://storageaccount.blob.core.windows.net/tempstorage/database__table__11111111-1111-1111-1111-111111111111__{}?".format( os.path.basename(f.name) ), format=data_format.kusto_value, ) mock_upload_blob_from_stream.assert_called() @responses.activate @patch("azure.kusto.data.security._AadHelper.acquire_authorization_header", return_value=None) @patch("azure.storage.blob.BlobClient.upload_blob") @patch("azure.storage.queue.QueueClient.send_message") @patch("uuid.uuid4", return_value=MOCKED_UUID_4) def test_fallback_big_stream(self, mock_uuid, mock_put_message_in_queue, mock_upload_blob_from_stream, mock_aad): responses.add_callback( responses.POST, "https://ingest-somecluster.kusto.windows.net/v1/rest/mgmt", callback=queued_request_callback, content_type="application/json" ) responses.add_callback( responses.POST, "https://somecluster.kusto.windows.net/v1/rest/ingest/database/table", callback=streaming_request_callback, content_type="application/json", ) data_format = DataFormat.ORC # Using orc to avoid compression ingest_client = ManagedStreamingIngestClient.from_engine_kcsb("https://somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=data_format) initial_bytes = bytearray(os.urandom(5 * 1024 * 1024)) stream = io.BytesIO(initial_bytes) def check_bytes(data): assert data.read() == initial_bytes mock_upload_blob_from_stream.side_effect = check_bytes result = ingest_client.ingest_from_stream(stream, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.QUEUED assert_queued_upload( mock_put_message_in_queue, mock_upload_blob_from_stream, "https://storageaccount.blob.core.windows.net/tempstorage/database__table__11111111-1111-1111-1111-111111111111__stream?", format=data_format.kusto_value, check_raw_data=False, ) mock_upload_blob_from_stream.assert_called() @responses.activate @patch( "azure.kusto.ingest.managed_streaming_ingest_client.ManagedStreamingIngestClient._create_exponential_retry", return_value=ExponentialRetry(ManagedStreamingIngestClient.ATTEMPT_COUNT, sleep_base_secs=0, max_jitter_secs=0), ) @patch("azure.kusto.data.security._AadHelper.acquire_authorization_header", return_value=None) @patch("azure.storage.blob.BlobClient.upload_blob") @patch("azure.storage.queue.QueueClient.send_message") @patch("uuid.uuid4", return_value=MOCKED_UUID_4) def test_fallback_transient_errors_limit(self, mock_uuid, mock_put_message_in_queue, mock_upload_blob_from_stream, mock_aad, mock_retry): total_attempts = 3 responses.add_callback( responses.POST, "https://ingest-somecluster.kusto.windows.net/v1/rest/mgmt", callback=queued_request_callback, content_type="application/json" ) ingest_client = ManagedStreamingIngestClient.from_engine_kcsb("https://somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table") helper = TransientResponseHelper(times_to_fail=total_attempts) responses.add_callback( responses.POST, "https://somecluster.kusto.windows.net/v1/rest/ingest/database/table", callback=lambda request: transient_error_callback(helper, request), content_type="application/json", ) # ensure test can work when executed from within directories current_dir = os.getcwd() path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.csv"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) result = ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.QUEUED assert_queued_upload( mock_put_message_in_queue, mock_upload_blob_from_stream, "https://storageaccount.blob.core.windows.net/tempstorage/database__table__11111111-1111-1111-1111-111111111111__dataset.csv.gz?", ) assert helper.total_calls == total_attempts @responses.activate @patch( "azure.kusto.ingest.managed_streaming_ingest_client.ManagedStreamingIngestClient._create_exponential_retry", return_value=ExponentialRetry(ManagedStreamingIngestClient.ATTEMPT_COUNT, sleep_base_secs=0, max_jitter_secs=0), ) @patch("azure.kusto.data.security._AadHelper.acquire_authorization_header", return_value=None) @patch("azure.storage.blob.BlobClient.upload_blob") @patch("azure.storage.queue.QueueClient.send_message") @patch("uuid.uuid4", return_value=MOCKED_UUID_4) def test_fallback_transient_single_error(self, mock_uuid, mock_put_message_in_queue, mock_upload_blob_from_stream, mock_aad, mock_retry): total_failures = 2 responses.add_callback( responses.POST, "https://ingest-somecluster.kusto.windows.net/v1/rest/mgmt", callback=queued_request_callback, content_type="application/json" ) helper = TransientResponseHelper(times_to_fail=total_failures) responses.add_callback( responses.POST, "https://somecluster.kusto.windows.net/v1/rest/ingest/database/table", callback=lambda request: transient_error_callback(helper, request), content_type="application/json", ) ingest_client = ManagedStreamingIngestClient.from_engine_kcsb("https://somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table") # ensure test can work when executed from within directories current_dir = os.getcwd() path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.csv"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) result = ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.SUCCESS assert helper.total_calls == total_failures + 1 @responses.activate def test_permanent_error(self): responses.add( responses.POST, "https://somecluster.kusto.windows.net/v1/rest/ingest/database/table", status=400, json={ "error": { "code": "BadRequest", "message": "Request is invalid and cannot be executed.", "@type": "Kusto.Common.Svc.Exceptions.AdminCommandWrongEndpointException", "@message": "Cannot get ingestion resources from this service endpoint. The appropriate endpoint is most likely " "'https://ingest-somecluster.kusto.windows.net/'.", "@context": { "timestamp": "2021-10-12T06:05:35.6602087Z", "serviceAlias": "SomeCluster", "machineName": "KEngine000000", "processName": "Kusto.WinSvc.Svc", "processId": 2648, "threadId": 472, "appDomainName": "Kusto.WinSvc.Svc.exe", "clientRequestId": "KPC.execute;a3dfb878-9d2b-49d6-89a5-e9b3a9f1f674", "activityId": "87eb8fc9-78b3-4580-bcc8-6c90482f9118", "subActivityId": "bbfb038b-4467-4f96-afd4-945904fc6278", "activityType": "DN.AdminCommand.IngestionResourcesGetCommand", "parentActivityId": "00e678e9-4204-4143-8c94-6afd94c27430", "activityStack": "(Activity stack: CRID=KPC.execute;a3dfb878-9d2b-49d6-89a5-e9b3a9f1f674 ARID=87eb8fc9-78b3-4580-bcc8-6c90482f9118 > DN.Admin.Client.ExecuteControlCommand/833dfb85-5d67-44b7-882d-eb2283e65780 > P.WCF.Service.ExecuteControlCommand..IInterNodeCommunicationAdminContract/3784e74f-1d89-4c15-adef-0a360c4c431e > DN.FE.ExecuteControlCommand/00e678e9-4204-4143-8c94-6afd94c27430 > DN.AdminCommand.IngestionResourcesGetCommand/bbfb038b-4467-4f96-afd4-945904fc6278)", }, "@permanent": True, } }, content_type="application/json", ) ingest_client = ManagedStreamingIngestClient.from_dm_kcsb("https://ingest-somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table", data_format=DataFormat.CSV) current_dir = os.getcwd() path_parts = ["azure-kusto-ingest", "tests", "input", "dataset.csv"] missing_path_parts = [] for path_part in path_parts: if path_part not in current_dir: missing_path_parts.append(path_part) file_path = os.path.join(current_dir, *missing_path_parts) with pytest.raises(KustoApiError) as ex: ingest_client.ingest_from_file(file_path, ingestion_properties=ingestion_properties) assert ex.value.get_api_error().permanent == True @responses.activate @patch("azure.kusto.data.security._AadHelper.acquire_authorization_header", return_value=None) @patch("azure.storage.queue.QueueClient.send_message") @patch("uuid.uuid4", return_value=MOCKED_UUID_4) def test_blob_ingestion(self, mock_uuid, mock_put_message_in_queue, mock_aad): responses.add_callback( responses.POST, "https://ingest-somecluster.kusto.windows.net/v1/rest/mgmt", callback=queued_request_callback, content_type="application/json" ) ingest_client = ManagedStreamingIngestClient.from_dm_kcsb("https://ingest-somecluster.kusto.windows.net") ingestion_properties = IngestionProperties(database="database", table="table") blob_path = ( "https://storageaccount.blob.core.windows.net/tempstorage/database__table__11111111-1111-1111-1111-111111111111__tmpbvk40leg?sp=rl&st=2020-05-20T13" "%3A38%3A37Z&se=2020-05-21T13%3A38%3A37Z&sv=2019-10-10&sr=c&sig=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx " ) result = ingest_client.ingest_from_blob(BlobDescriptor(blob_path, 1), ingestion_properties=ingestion_properties) assert result.status == IngestionStatus.QUEUED assert_queued_upload( mock_put_message_in_queue, mock_upload_blob_from_stream=None, expected_url="https://storageaccount.blob.core.windows.net/tempstorage/database__table__11111111-1111-1111-1111-111111111111__tmpbvk40leg?", ) azure-kusto-python-3.0.1/azure-kusto-ingest/tests/test_status_q.py000066400000000000000000000254431417222763000255350ustar00rootroot00000000000000# Copyright (c) Microsoft Corporation. # Licensed under the MIT License import json import time import unittest from uuid import uuid4 import mock from azure.kusto.ingest import QueuedIngestClient from azure.kusto.ingest._resource_manager import _ResourceUri from azure.kusto.ingest.status import KustoIngestStatusQueues, SuccessMessage, FailureMessage from azure.storage.queue import QueueMessage, QueueClient ENDPOINT_SUFFIX = "sp=rl&st=2020-05-20T13:38:37Z&se=2020-05-21T13:38:37Z&sv=2019-10-10&sr=c&sig=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" OBJECT_TYPE = "core.windows.net" def mock_message(success): m = QueueMessage() m.id = uuid4() m.insertion_time = time.time() m.expiration_time = None m.dequeue_count = None if success: content = { "OperationId": str(m.id), "Database": "db1", "Table": "table1", "IngestionSourceId": str(m.id), "IngestionSourcePath": "blob/path", "RootActivityId": "1", "SucceededOn": time.time(), } else: content = { "OperationId": str(m.id), "Database": "db1", "Table": "table1", "IngestionSourceId": str(m.id), "IngestionSourcePath": "blob/path", "RootActivityId": "1", "FailedOn": time.time(), "Details": "", "ErrorCode": "1", "FailureStatus": "", "OriginatesFromUpdatePolicy": "", "ShouldRetry": False, } m.content = json.dumps(content) m.pop_receipt = None m.time_next_visible = None return m def fake_peek_factory(f): def fake_peek(self, max_messages): return f(self.queue_name, max_messages) return fake_peek def fake_receive_factory(f): def fake_receive(self, messages_per_page, *args, **kwargs): return f(self.queue_name, messages_per_page) return fake_receive def fake_delete_factory(f): def fake_delete(self, n): return f(self.queue_name, n) return fake_delete class StatusQTests(unittest.TestCase): def test_init(self): client = QueuedIngestClient("some-cluster") qs = KustoIngestStatusQueues(client) assert qs.success.message_cls == SuccessMessage assert qs.failure.message_cls == FailureMessage def test_isempty(self): client = QueuedIngestClient("some-cluster") fake_peek = fake_peek_factory( lambda queue_name, num_messages=1: [mock_message(success=True) for _ in range(0, num_messages)] if "qs" in queue_name else [] ) with mock.patch.object(client._resource_manager, "get_successful_ingestions_queues") as mocked_get_success_qs, mock.patch.object( client._resource_manager, "get_failed_ingestions_queues" ) as mocked_get_failed_qs, mock.patch.object(QueueClient, "peek_messages", autospec=True, side_effect=fake_peek) as q_mock: fake_failed_queue = _ResourceUri( "mocked_storage_account1", OBJECT_TYPE, "queue", "mocked_qf_name", ENDPOINT_SUFFIX, ) fake_success_queue = _ResourceUri( "mocked_storage_account2", OBJECT_TYPE, "queue", "mocked_qs_name", ENDPOINT_SUFFIX, ) mocked_get_success_qs.return_value = [fake_success_queue] mocked_get_failed_qs.return_value = [fake_failed_queue] qs = KustoIngestStatusQueues(client) assert qs.success.is_empty() is False assert qs.failure.is_empty() is True assert q_mock.call_count == 2 assert q_mock.call_args_list[0][1]["max_messages"] == 2 assert q_mock.call_args_list[1][1]["max_messages"] == 2 def test_peek(self): client = QueuedIngestClient("some-cluster") fake_peek = fake_peek_factory( lambda queue_name, num_messages=1: [ mock_message(success=True) if "qs" in queue_name else mock_message(success=False) for _ in range(0, num_messages) ] ) with mock.patch.object(client._resource_manager, "get_successful_ingestions_queues") as mocked_get_success_qs, mock.patch.object( client._resource_manager, "get_failed_ingestions_queues" ) as mocked_get_failed_qs, mock.patch.object(QueueClient, "peek_messages", autospec=True, side_effect=fake_peek) as q_mock: fake_failed_queue1 = _ResourceUri( "mocked_storage_account_f1", OBJECT_TYPE, "queue", "mocked_qf_1_name", ENDPOINT_SUFFIX, ) fake_failed_queue2 = _ResourceUri( "mocked_storage_account_f2", OBJECT_TYPE, "queue", "mocked_qf_2_name", ENDPOINT_SUFFIX, ) fake_success_queue = _ResourceUri( "mocked_storage_account2", OBJECT_TYPE, "queue", "mocked_qs_name", ENDPOINT_SUFFIX, ) mocked_get_success_qs.return_value = [fake_success_queue] mocked_get_failed_qs.return_value = [fake_failed_queue1, fake_failed_queue2] qs = KustoIngestStatusQueues(client) peek_success_actual = qs.success.peek() peek_failure_actual = qs.failure.peek(6) assert len(peek_success_actual) == 1 for m in peek_failure_actual: assert isinstance(m, FailureMessage) is True for m in peek_success_actual: assert isinstance(m, SuccessMessage) is True assert len(peek_failure_actual) == 6 actual = {} assert len(QueueClient.peek_messages.call_args_list) == 3 for call_args in q_mock.call_args_list: actual[call_args[0][0].queue_name] = actual.get(call_args[0][0].queue_name, 0) + call_args[1]["max_messages"] assert actual[fake_failed_queue2.object_name] == 4 assert actual[fake_failed_queue1.object_name] == 4 assert actual[fake_success_queue.object_name] == 2 def test_pop(self): client = QueuedIngestClient("some-cluster") fake_receive = fake_receive_factory( lambda queue_name, num_messages=1: [ mock_message(success=True) if "qs" in queue_name else mock_message(success=False) for _ in range(0, num_messages) ] ) with mock.patch.object(client._resource_manager, "get_successful_ingestions_queues") as mocked_get_success_qs, mock.patch.object( client._resource_manager, "get_failed_ingestions_queues" ) as mocked_get_failed_qs, mock.patch.object( QueueClient, "receive_messages", autospec=True, side_effect=fake_receive, ) as q_receive_mock, mock.patch.object( QueueClient, "delete_message", return_value=None ) as q_del_mock: fake_failed_queue1 = _ResourceUri( "mocked_storage_account_f1", OBJECT_TYPE, "queue", "mocked_qf_1_name", ENDPOINT_SUFFIX, ) fake_failed_queue2 = _ResourceUri( "mocked_storage_account_f2", OBJECT_TYPE, "queue", "mocked_qf_2_name", ENDPOINT_SUFFIX, ) fake_success_queue = _ResourceUri( "mocked_storage_account2", OBJECT_TYPE, "queue", "mocked_qs_name", ENDPOINT_SUFFIX, ) mocked_get_success_qs.return_value = [fake_success_queue] mocked_get_failed_qs.return_value = [fake_failed_queue1, fake_failed_queue2] qs = KustoIngestStatusQueues(client) get_success_actual = qs.success.pop() get_failure_actual = qs.failure.pop(6) assert len(get_success_actual) == 1 assert len(get_failure_actual) == 6 for m in get_failure_actual: assert isinstance(m, FailureMessage) for m in get_success_actual: assert isinstance(m, SuccessMessage) assert q_receive_mock.call_count == 3 assert q_del_mock.call_count == len(get_success_actual) + len(get_failure_actual) assert q_receive_mock.call_args_list[0][1]["messages_per_page"] == 2 actual = { q_receive_mock.call_args_list[1][0][0].queue_name: q_receive_mock.call_args_list[1][1]["messages_per_page"], q_receive_mock.call_args_list[2][0][0].queue_name: q_receive_mock.call_args_list[2][1]["messages_per_page"], } assert actual[fake_failed_queue2.object_name] == 4 assert actual[fake_failed_queue1.object_name] == 4 def test_pop_unbalanced_queues(self): client = QueuedIngestClient("some-cluster") fake_receive = fake_receive_factory( lambda queue_name, messages_per_page=1: [mock_message(success=False) for _ in range(0, messages_per_page)] if "1" in queue_name else [] ) with mock.patch.object(client._resource_manager, "get_successful_ingestions_queues"), mock.patch.object( client._resource_manager, "get_failed_ingestions_queues" ) as mocked_get_failed_qs, mock.patch.object( QueueClient, "receive_messages", autospec=True, side_effect=fake_receive, ) as q_receive_mock, mock.patch.object( QueueClient, "delete_message", return_value=None ): fake_failed_queue1 = _ResourceUri( "mocked_storage_account_f1", OBJECT_TYPE, "queue", "mocked_qf_1_name", ENDPOINT_SUFFIX, ) fake_failed_queue2 = _ResourceUri( "mocked_storage_account_f2", OBJECT_TYPE, "queue", "mocked_qf_2_name", ENDPOINT_SUFFIX, ) mocked_get_failed_qs.return_value = [fake_failed_queue1, fake_failed_queue2] qs = KustoIngestStatusQueues(client) get_failure_actual = qs.failure.pop(6) assert len(get_failure_actual) == 6 for m in get_failure_actual: assert isinstance(m, FailureMessage) assert q_receive_mock.call_count == 3 actual = {} for call_args in q_receive_mock.call_args_list: actual[call_args[0][0].queue_name] = actual.get(call_args[0][0].queue_name, 0) + call_args[1]["messages_per_page"] assert actual[fake_failed_queue2.object_name] + actual[fake_failed_queue1.object_name] == (4 + 4 + 6) azure-kusto-python-3.0.1/azure-kusto-ingest/tox.ini000066400000000000000000000001311417222763000224150ustar00rootroot00000000000000[tox] envlist = py27,py35,py37 [testenv] deps=mock pytest pandas commands=pytestazure-kusto-python-3.0.1/back_to_black.bat000066400000000000000000000000711417222763000205530ustar00rootroot00000000000000pip install --upgrade black black . --line-length 160 azure-kusto-python-3.0.1/build_packages.py000066400000000000000000000045321417222763000206420ustar00rootroot00000000000000import argparse import os from subprocess import check_call from pathlib import Path try: from packaging.version import parse as Version, InvalidVersion except ImportError: # Should not happen, but at worst in most case this is the same from pip._vendor.packaging.version import parse as Version, InvalidVersion DEFAULT_DESTINATION_FOLDER = os.path.join("..", "dist") package_list = ["azure-kusto-data", "azure-kusto-ingest"] def create_package(name, dest_folder=DEFAULT_DESTINATION_FOLDER): absdirpath = os.path.abspath(name) check_call(["python", "setup.py", "bdist_wheel", "-d", dest_folder], cwd=absdirpath) check_call(["python", "setup.py", "sdist", "-d", dest_folder], cwd=absdirpath) def travis_build_package(): """Assumed called on Travis, to prepare a package to be deployed This method prints on stdout for Travis. Return is obj to pass to sys.exit() directly """ travis_tag = os.environ.get("TRAVIS_TAG") if not travis_tag: print("TRAVIS_TAG environment variable is not present") return "TRAVIS_TAG environment variable is not present" try: version = Version(travis_tag) except InvalidVersion: failure = "Version must be a valid PEP440 version (version is: {})".format(version) print(failure) return failure abs_dist_path = Path(os.environ["TRAVIS_BUILD_DIR"], "dist") [create_package(package, str(abs_dist_path)) for package in package_list] print("Produced:\n{}".format(list(abs_dist_path.glob("*")))) pattern = "*{}*".format(version) packages = list(abs_dist_path.glob(pattern)) if not packages: return "Package version does not match tag {}, abort".format(version) pypi_server = os.environ.get("PYPI_SERVER", "default PyPI server") print("Package created as expected and will be pushed to {}".format(pypi_server)) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Build Azure package.") parser.add_argument("name", help="The package name") parser.add_argument("--dest", "-d", default=DEFAULT_DESTINATION_FOLDER, help="Destination folder. Relative to the package dir. [default: %(default)s]") args = parser.parse_args() if args.name == "all": for package in package_list: create_package(package, args.dest) else: create_package(args.name, args.dest) azure-kusto-python-3.0.1/dev_requirements.txt000066400000000000000000000002441417222763000214510ustar00rootroot00000000000000pytest>=3.2.0 mock>=2.0.0 responses>=0.9.0 pandas>=0.24.0 black;python_version >= '3.6' aioresponses>=0.6.2 pytest-asyncio>=0.12.0 azure-core>=1.11.0 asgiref>=3.2.3azure-kusto-python-3.0.1/pyproject.toml000066400000000000000000000000361417222763000202420ustar00rootroot00000000000000[tool.black] line-length = 160azure-kusto-python-3.0.1/quick_start/000077500000000000000000000000001417222763000176605ustar00rootroot00000000000000azure-kusto-python-3.0.1/quick_start/README.md000066400000000000000000000045331417222763000211440ustar00rootroot00000000000000# Quickstart App The quick start application is a **self-contained and runnable** example script that demonstrates authenticating, connecting to, administering, ingesting data into and querying Azure Data Explorer using the azure-kusto-python SDK. You can use it as a baseline to write your own first kusto client application, altering the code as you go, or copy code sections out of it into your app. **Tip:** The app includes comments with tips on recommendations, coding best practices, links to reference materials and recommended TODO changes when adapting the code to your needs. ## Using the App for the first time ### Prerequisites 1. Set up Python version 3.7 or higher on your machine. For instructions, consult a Python environment setup tutorial, like [this](https://www. tutorialspoint.com/java/java_environment_setup.htm). ### Retrieving the app from GitHub 1. Download the app files from this GitHub repo. 1. Modify the `kusto_sample_config.json` file, changing `KustoUri`, `IngestUri` and `DatabaseName` appropriately for your cluster. ### Retrieving the app from OneClick 1. Open a browser and type your cluster's URL (e.g. https://mycluster.westeurope.kusto.windows.net/). You will be redirected to the _Azure Data Explorer_ Web UI. 1. On the left menu, select **Data**. 1. Click **Generate Sample App Code** and then follow the instructions in the wizard. 1. Download the app as a ZIP file. 1. Extract the app source code. **Note**: The configuration parameters defined in the `kusto_sample_config.json` file are preconfigured with the appropriate values for your cluster. Verify that these are correct. ### Run the app 1. Open a command line window and navigate to the folder where you extracted the script. 1. Run `python -m pip install azure-kusto-data azure-kusto-ingest`. If upgrading, append ` --upgrade`. 1. Run `python kusto_sample_app.py`. #### Troubleshooting * If you are having trouble running the script from your IDE, first check if the script runs from the command line, then consult the troubleshooting references of your IDE. ### Optional changes You can make the following changes from within the script itself: - Change the default User-Prompt authentication method by editing `AUTHENTICATION_MODE`. - Change the script to run without stopping between steps by setting `WAIT_FOR_USER = False`azure-kusto-python-3.0.1/quick_start/dataset.csv000066400000000000000000000027531417222763000220310ustar00rootroot000000000000000,00000000-0000-0000-0001-020304050607,0,0,0,0,0,0,0,0,0,0,2014-01-01T01:01:01.0000000Z,Zero,Zero,0,00:00:00,,null 1,00000001-0000-0000-0001-020304050607,1.0001,1.01,1,1,1,1,1,1,1,1,2015-01-01T01:01:01.0000000Z,One,One,1,00:01.0,,"{""rowId"": 1, ""arr"": [0,1]}" 2,00000002-0000-0000-0001-020304050607,2.0002,2.02,0,2,2,2,2,2,2,2,2016-01-01T01:01:01.0000000Z,Two,Two,2,-00:00:02.0020002,,"{""rowId"": 2, ""arr"": [0,2]}" 3,00000003-0000-0000-0001-020304050607,3.0003,3.03,1,3,3,3,3,3,3,3,2017-01-01T01:01:01.0000000Z,Three,Three,3,00:03.0,,"{""rowId"": 3, ""arr"": [0,3]}" 4,00000004-0000-0000-0001-020304050607,4.0004,4.04,0,4,4,4,4,4,4,4,2018-01-01T01:01:01.0000000Z,Four,Four,4,-00:00:04.0040004,,"{""rowId"": 4, ""arr"": [0,4]}" 5,00000005-0000-0000-0001-020304050607,5.0005,5.05,1,5,5,5,5,5,5,5,2019-01-01T01:01:01.0000000Z,Five,Five,5,00:05.0,,"{""rowId"": 5, ""arr"": [0,5]}" 6,00000006-0000-0000-0001-020304050607,6.0006,6.06,0,6,6,6,6,6,6,6,2020-01-01T01:01:01.0000000Z,Six,Six,6,-00:00:06.0060006,,"{""rowId"": 6, ""arr"": [0,6]}" 7,00000007-0000-0000-0001-020304050607,7.0007,7.07,1,7,7,7,7,7,7,7,2021-01-01T01:01:01.0000000Z,Seven,Seven,7,00:07.0,,"{""rowId"": 7, ""arr"": [0,7]}" 8,00000008-0000-0000-0001-020304050607,8.0008,8.08,0,8,8,8,8,8,8,8,2022-01-01T01:01:01.0000000Z,Eight,Eight,8,-00:00:08.0080008,,"{""rowId"": 8, ""arr"": [0,8]}" 9,00000009-0000-0000-0001-020304050607,9.0009,9.09,1,9,9,9,9,9,9,9,2023-01-01T01:01:01.0000000Z,Nine,Nine,9,00:09.0,,"{""rowId"": 9, ""arr"": [0,9]}" azure-kusto-python-3.0.1/quick_start/dataset.json000066400000000000000000000014101417222763000221740ustar00rootroot00000000000000{"rownumber": 0, "rowguid": "00000000-0000-0000-0001-020304050607", "xdouble": 0.0, "xfloat": 0.0, "xbool": 0, "xint16": 0, "xint32": 0, "xint64": 0, "xunit8": 0, "xuint16": 0, "xunit32": 0, "xunit64": 0, "xdate": "2014-01-01T01:01:01Z", "xsmalltext": "Zero", "xtext": "Zero", "xnumberAsText": "0", "xtime": "00:00:00", "xtextWithNulls": null, "xdynamicWithNulls": ""} {"rownumber": 1, "rowguid": "00000001-0000-0000-0001-020304050607", "xdouble": 1.00001, "xfloat": 1.01, "xbool": 1, "xint16": 1, "xint32": 1, "xint64": 1, "xuint8": 1, "xuint16": 1, "xuint32": 1, "xuint64": 1, "xdate": "2015-01-01T01:01:01Z", "xsmalltext": "One", "xtext": "One", "xnumberAsText": "1", "xtime": "00:00:01.0010001", "xtextWithNulls": null, "xdynamicWithNulls": "{\"rowId\":1,\"arr\":[0,1]}"}azure-kusto-python-3.0.1/quick_start/kusto_sample_app.py000066400000000000000000000577461417222763000236230ustar00rootroot00000000000000import json import os import time import uuid from distutils.util import strtobool from typing import ClassVar from azure.kusto.data import KustoClient, KustoConnectionStringBuilder, ClientRequestProperties from azure.kusto.data.data_format import DataFormat from azure.kusto.data.exceptions import KustoClientError, KustoServiceError from azure.kusto.ingest import ( QueuedIngestClient, IngestionProperties, FileDescriptor, BlobDescriptor, BaseIngestClient, ) class KustoSampleApp: # TODO (config - optional): Change the authentication method from "User Prompt" to any of the other options # Some of the auth modes require additional environment variables to be set in order to work (see usage in generate_connection_string below) # Managed Identity Authentication only works when running as an Azure service (webapp, function, etc.) AUTHENTICATION_MODE = "UserPrompt" # Options: (UserPrompt|ManagedIdentity|AppKey|AppCertificate) # TODO (config - optional): Toggle to False to execute this script "unattended" WAIT_FOR_USER = True # TODO (config): # If this quickstart app was downloaded from OneClick, kusto_sample_config.json should be pre-populated with your cluster's details # If this quickstart app was downloaded from GitHub, edit kusto_sample_config.json and modify the cluster URL and database fields appropriately CONFIG_FILE_NAME = "kusto_sample_config.json" BATCHING_POLICY = '{ "MaximumBatchingTimeSpan": "00:00:10", "MaximumNumberOfItems": 500, "MaximumRawDataSizeMB": 1024 }' WAIT_FOR_INGEST_SECONDS = 20 _step = 1 use_existing_table: ClassVar[bool] database_name: ClassVar[str] table_name: ClassVar[str] table_schema: ClassVar[str] kusto_url: ClassVar[str] ingest_url: ClassVar[str] data_to_ingest: ClassVar[list] should_alter_table: ClassVar[bool] should_query_data: ClassVar[bool] should_ingest_data: ClassVar[bool] @classmethod def start(cls) -> None: cls.load_configs(cls.CONFIG_FILE_NAME) if cls.AUTHENTICATION_MODE == "UserPrompt": cls.wait_for_user_to_proceed("You will be prompted *twice* for credentials during this script. Please return to the console after authenticating.") kusto_connection_string = cls.generate_connection_string(cls.kusto_url, cls.AUTHENTICATION_MODE) ingest_connection_string = cls.generate_connection_string(cls.ingest_url, cls.AUTHENTICATION_MODE) # Tip: Avoid creating a new Kusto/ingest client for each use. Instead, create the clients once and reuse them. kusto_client = KustoClient(kusto_connection_string) ingest_client = QueuedIngestClient(ingest_connection_string) if cls.use_existing_table: if cls.should_alter_table: # Tip: Usually table was originally created with a schema appropriate for the data being ingested, so this wouldn't be needed. # Learn More: For more information about altering table schemas, see: https://docs.microsoft.com/azure/data-explorer/kusto/management/alter-table-command cls.alter_merge_existing_table_to_provided_schema(kusto_client, cls.database_name, cls.table_name, cls.table_schema) if cls.should_query_data: # Learn More: For more information about Kusto Query Language (KQL), see: https://docs.microsoft.com/azure/data-explorer/write-queries cls.query_existing_number_of_rows(kusto_client, cls.database_name, cls.table_name) else: # Tip: This is generally a one-time configuration # Learn More: For more information about creating tables, see: https://docs.microsoft.com/azure/data-explorer/one-click-table cls.create_new_table(kusto_client, cls.database_name, cls.table_name, cls.table_schema) if cls.should_ingest_data: for file in cls.data_to_ingest: data_format = DataFormat[str(file["format"]).upper()] mapping_name = file["mappingName"] # Tip: This is generally a one-time configuration. # Learn More: For more information about providing inline mappings and mapping references, see: https://docs.microsoft.com/azure/data-explorer/kusto/management/mappings if not cls.create_ingestion_mappings( strtobool(str(file["useExistingMapping"]).lower()), kusto_client, cls.database_name, cls.table_name, mapping_name, file["mappingValue"], data_format, ): continue # Learn More: For more information about ingesting data to Kusto in Python, see: https://docs.microsoft.com/azure/data-explorer/python-ingest-data cls.ingest(file, data_format, ingest_client, cls.database_name, cls.table_name, mapping_name) cls.wait_for_ingestion_to_complete() if cls.should_query_data: cls.execute_validation_queries(kusto_client, cls.database_name, cls.table_name, cls.should_ingest_data) @classmethod def wait_for_user_to_proceed(cls, upcoming_operation: str) -> None: print() print(f"Step {cls._step}: {upcoming_operation}") cls._step = cls._step + 1 if cls.WAIT_FOR_USER: input("Press ENTER to proceed with this operation...") @classmethod def die(cls, error: str, ex: Exception = None) -> None: print(f"Script failed with error: {error}") if ex is not None: print("Exception:") print(ex) exit(-1) @classmethod def load_configs(cls, config_file_name: str) -> None: try: with open(config_file_name, "r") as config_file: config = json.load(config_file) except Exception as ex: cls.die(f"Couldn't read load config file from file '{config_file_name}'", ex) cls.use_existing_table = strtobool(str(config["useExistingTable"]).lower()) cls.database_name = config["databaseName"] cls.table_name = config["tableName"] cls.table_schema = config["tableSchema"] cls.kusto_url = config["kustoUri"] cls.ingest_url = config["ingestUri"] cls.data_to_ingest = config["data"] cls.should_alter_table = strtobool(str(config["alterTable"]).lower()) cls.should_query_data = strtobool(str(config["queryData"]).lower()) cls.should_ingest_data = strtobool(str(config["ingestData"]).lower()) if ( cls.database_name is None or cls.table_name is None or cls.table_schema is None or cls.kusto_url is None or cls.ingest_url is None or cls.data_to_ingest is None ): cls.die(f"File '{config_file_name}' is missing required fields") @classmethod def generate_connection_string(cls, cluster_url: str, authentication_mode: str) -> KustoConnectionStringBuilder: # Learn More: For additional information on how to authorize users and apps in Kusto, see: https://docs.microsoft.com/azure/data-explorer/manage-database-permissions if authentication_mode == "UserPrompt": # Prompt user for credentials return KustoConnectionStringBuilder.with_interactive_login(cluster_url) elif authentication_mode == "ManagedIdentity": return cls.create_managed_identity_connection_string(cluster_url) elif authentication_mode == "AppKey": # Learn More: For information about how to procure an AAD Application, see: https://docs.microsoft.com/azure/data-explorer/provision-azure-ad-app # TODO (config - optional): App ID & tenant, and App Key to authenticate with return KustoConnectionStringBuilder.with_aad_application_key_authentication( cluster_url, os.environ.get("APP_ID"), os.environ.get("APP_KEY"), os.environ.get("APP_TENANT") ) elif authentication_mode == "AppCertificate": return cls.create_application_certificate_connection_string(cluster_url) else: cls.die(f"Authentication mode '{authentication_mode}' is not supported") @classmethod def create_managed_identity_connection_string(cls, cluster_url: str) -> KustoConnectionStringBuilder: # Connect using the system- or user-assigned managed identity (Azure service only) # TODO (config - optional): Managed identity client ID if you are using a user-assigned managed identity client_id = os.environ.get("MANAGED_IDENTITY_CLIENT_ID") if client_id is None: return KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(cluster_url) else: return KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(cluster_url, client_id=client_id) @classmethod def create_application_certificate_connection_string(cls, cluster_url: str) -> KustoConnectionStringBuilder: # TODO (config - optional): App ID & tenant, path to public certificate and path to private certificate pem file to authenticate with app_id = os.environ.get("APP_ID") app_tenant = os.environ.get("APP_TENANT") private_key_pem_file_path = os.environ.get("PRIVATE_KEY_PEM_FILE_PATH") cert_thumbprint = os.environ.get("CERT_THUMBPRINT") public_cert_file_path = os.environ.get("PUBLIC_CERT_FILE_PATH") # Only used for "Subject Name and Issuer" auth public_certificate = None pem_certificate = None try: with open(private_key_pem_file_path, "r") as pem_file: pem_certificate = pem_file.read() except Exception as ex: cls.die(f"Failed to load PEM file from {private_key_pem_file_path}", ex) if public_cert_file_path: try: with open(public_cert_file_path, "r") as cert_file: public_certificate = cert_file.read() except Exception as ex: cls.die(f"Failed to load public certificate file from {public_cert_file_path}", ex) return KustoConnectionStringBuilder.with_aad_application_certificate_sni_authentication( cluster_url, app_id, pem_certificate, public_certificate, cert_thumbprint, app_tenant ) else: return KustoConnectionStringBuilder.with_aad_application_certificate_authentication( cluster_url, app_id, pem_certificate, cert_thumbprint, app_tenant ) @classmethod def create_new_table(cls, kusto_client: KustoClient, database_name: str, table_name: str, table_schema: str) -> None: cls.wait_for_user_to_proceed(f"Create table '{database_name}.{table_name}'") command = f".create table {table_name} {table_schema}" if not cls.execute_control_command(kusto_client, database_name, command): cls.die(f"Failed to create table or validate it exists using command '{command}'") # Learn More: # Kusto batches data for ingestion efficiency. The default batching policy ingests data when one of the following conditions are met: # 1) More than 1,000 files were queued for ingestion for the same table by the same user # 2) More than 1GB of data was queued for ingestion for the same table by the same user # 3) More than 5 minutes have passed since the first file was queued for ingestion for the same table by the same user # For more information about customizing the ingestion batching policy, see: https://docs.microsoft.com/azure/data-explorer/kusto/management/batchingpolicy # Disabled to prevent an existing batching policy from being unintentionally changed if False: cls.alter_batching_policy(kusto_client, database_name, table_name) @classmethod def alter_merge_existing_table_to_provided_schema(cls, kusto_client: KustoClient, database_name: str, table_name: str, table_schema: str) -> None: cls.wait_for_user_to_proceed(f"Alter-merge existing table '{database_name}.{table_name}' to align with the provided schema") command = f".alter-merge table {table_name} {table_schema}" if not cls.execute_control_command(kusto_client, database_name, command): cls.die(f"Failed to alter table using command '{command}'") @classmethod def execute_control_command(cls, client: KustoClient, database_name: str, control_command: str) -> bool: try: client_request_properties = cls.create_client_request_properties("Python_SampleApp_ControlCommand") result = client.execute_mgmt(database_name, control_command, client_request_properties) # Tip: Actual implementations wouldn't generally print the response from a control command. We print here to demonstrate what the response looks like. print(f"Response from executed control command '{control_command}':") for row in result.primary_results[0]: print(row.to_list()) return True except KustoClientError as ex: print(f"Client error while trying to execute control command '{control_command}' on database '{database_name}'") print(ex) except KustoServiceError as ex: print(f"Server error while trying to execute control command '{control_command}' on database '{database_name}'") print(ex) except Exception as ex: print(f"Unknown error while trying to execute control command '{control_command}' on database '{database_name}'") print(ex) return False @classmethod def execute_query(cls, kusto_client: KustoClient, database_name: str, query: str): try: client_request_properties = cls.create_client_request_properties("Python_SampleApp_Query") result = kusto_client.execute_query(database_name, query, client_request_properties) print(f"Response from executed query '{query}':") for row in result.primary_results[0]: print(row.to_list()) return True except KustoClientError as ex: print(f"Client error while trying to execute query '{query}' on database '{database_name}'") print(ex) except KustoServiceError as ex: print(f"Server error while trying to execute query '{query}' on database '{database_name}'") print(ex) except Exception as ex: print(f"Unknown error while trying to execute query '{query}' on database '{database_name}'") print(ex) return False @classmethod def create_client_request_properties(cls, scope: str, timeout: str = None) -> ClientRequestProperties: client_request_properties = ClientRequestProperties() client_request_properties.client_request_id = f"{scope};{str(uuid.uuid4())}" client_request_properties.application = "kusto_sample_app.py" # Tip: Though uncommon, you can alter the request default command timeout using the below command, e.g. to set the timeout to 10 minutes, use "10m" if timeout is not None: client_request_properties.set_option(ClientRequestProperties.request_timeout_option_name, timeout) return client_request_properties @classmethod def query_existing_number_of_rows(cls, kusto_client: KustoClient, database_name: str, table_name: str) -> None: cls.wait_for_user_to_proceed(f"Get existing row count in '{database_name}.{table_name}'") cls.execute_query(kusto_client, database_name, f"{table_name} | count") @classmethod def alter_batching_policy(cls, kusto_client: KustoClient, database_name: str, table_name: str) -> None: # Tip 1: Though most users should be fine with the defaults, to speed up ingestion, such as during development and # in this sample app, we opt to modify the default ingestion policy to ingest data after at most 10 seconds. # Tip 2: This is generally a one-time configuration. # Tip 3: You can also skip the batching for some files using the Flush-Immediately property, though this option should be used with care as it is inefficient. cls.wait_for_user_to_proceed(f"Alter the batching policy for table '{database_name}.{table_name}'") command = f".alter table {table_name} policy ingestionbatching @'{cls.BATCHING_POLICY}'" if not cls.execute_control_command(kusto_client, database_name, command): print( "Failed to alter the ingestion policy, which could be the result of insufficient permissions. The sample will still run, though ingestion will be delayed for up to 5 minutes." ) @classmethod def create_ingestion_mappings( cls, use_existing_mapping: bool, kusto_client: KustoClient, database_name: str, table_name: str, mapping_name: str, mapping_value: str, data_format: DataFormat, ) -> bool: if not use_existing_mapping: if data_format._mapping_required and not mapping_value: print( f"The data format '{data_format.kusto_value}' requires a mapping, but configuration indicates to not use an existing mapping and no mapping was provided. Skipping this ingestion." ) return False if mapping_value: ingestion_mapping_kind = data_format.ingestion_mapping_kind.value.lower() cls.wait_for_user_to_proceed(f"Create a '{ingestion_mapping_kind}' mapping reference named '{mapping_name}'") if not mapping_name: mapping_name = "DefaultQuickstartMapping" + str(uuid.UUID())[:5] mapping_command = f".create-or-alter table {table_name} ingestion {ingestion_mapping_kind} mapping '{mapping_name}' '{mapping_value}'" if not cls.execute_control_command(kusto_client, database_name, mapping_command): print(f"Failed to create a '{ingestion_mapping_kind}' mapping reference named '{mapping_name}'. Skipping this ingestion.") return False elif data_format._mapping_required and not mapping_name: print( f"The data format '{data_format.kusto_value}' requires a mapping and the configuration indicates an existing mapping should be used, but none was provided. Skipping this ingestion." ) return False return True @classmethod def ingest(cls, file: dict, data_format: DataFormat, ingest_client: BaseIngestClient, database_name: str, table_name: str, mapping_name: str) -> None: source_type = str(file["sourceType"]).lower() uri = file["dataSourceUri"] cls.wait_for_user_to_proceed(f"Ingest '{uri}' from '{source_type}'") # Tip: When ingesting json files, if each line represents a single-line json, use MULTIJSON format even if the file only contains one line. # If the json contains whitespace formatting, use SINGLEJSON. In this case, only one data row json object is allowed per file. data_format = DataFormat.MULTIJSON if data_format == data_format.JSON else data_format # Tip: Kusto's Python SDK can ingest data from files, blobs, open streams and pandas dataframes. # See the SDK's samples and the E2E tests in azure.kusto.ingest for additional references. if source_type == "localfilesource": cls.ingest_from_file(ingest_client, database_name, table_name, uri, data_format, mapping_name) elif source_type == "blobsource": ingest_client: QueuedIngestClient cls.ingest_from_blob(ingest_client, database_name, table_name, uri, data_format, mapping_name) else: print(f"Unknown source '{source_type}' for file '{uri}'") @classmethod def ingest_from_file( cls, ingest_client: BaseIngestClient, database_name: str, table_name: str, file_path: str, data_format: DataFormat, mapping_name: str = None ): ingestion_properties = cls.create_ingestion_properties(database_name, table_name, data_format, mapping_name) # Tip 1: For optimal ingestion batching and performance, specify the uncompressed data size in the file descriptor instead of the default below of 0. # Otherwise, the service will determine the file size, requiring an additional s2s call, and may not be accurate for compressed files. # Tip 2: To correlate between ingestion operations in your applications and Kusto, set the source ID and log it somewhere file_descriptor = FileDescriptor(file_path, size=0, source_id=uuid.uuid4()) ingest_client.ingest_from_file(file_descriptor, ingestion_properties=ingestion_properties) @classmethod def ingest_from_blob( cls, client: QueuedIngestClient, database_name: str, table_name: str, blob_url: str, data_format: DataFormat, mapping_name: str = None ): ingestion_properties = cls.create_ingestion_properties(database_name, table_name, data_format, mapping_name) # Tip 1: For optimal ingestion batching and performance, specify the uncompressed data size in the file descriptor instead of the default below of 0. # Otherwise, the service will determine the file size, requiring an additional s2s call, and may not be accurate for compressed files. # Tip 2: To correlate between ingestion operations in your applications and Kusto, set the source ID and log it somewhere blob_descriptor = BlobDescriptor(blob_url, size=0, source_id=str(uuid.uuid4())) client.ingest_from_blob(blob_descriptor, ingestion_properties=ingestion_properties) @classmethod def create_ingestion_properties(cls, database_name: str, table_name: str, data_format: DataFormat, mapping_name: str) -> IngestionProperties: return IngestionProperties( database=f"{database_name}", table=f"{table_name}", ingestion_mapping_reference=mapping_name, # Learn More: For more information about supported data formats, see: https://docs.microsoft.com/azure/data-explorer/ingestion-supported-formats data_format=data_format, # TODO (config - optional): Setting the ingestion batching policy takes up to 5 minutes to take effect. # We therefore set Flush-Immediately for the sake of the sample, but it generally shouldn't be used in practice. # Comment out the line below after running the sample the first few times. flush_immediately=True, ) @classmethod def wait_for_ingestion_to_complete(cls) -> None: print( f"Sleeping {cls.WAIT_FOR_INGEST_SECONDS} seconds for queued ingestion to complete. Note: This may take longer depending on the file size and ingestion batching policy." ) for x in range(cls.WAIT_FOR_INGEST_SECONDS, 0, -1): print(f"{x} ", end="\r") time.sleep(1) print() print() @classmethod def execute_validation_queries(cls, kusto_client: KustoClient, database_name: str, table_name: str, should_ingest_data: bool) -> None: optional_post_ingestion_message = "post-ingestion " if should_ingest_data else "" print(f"Step {cls._step}: Get {optional_post_ingestion_message}row count for '{database_name}.{table_name}':") cls._step = cls._step + 1 cls.execute_query(kusto_client, database_name, f"{table_name} | count") print("") print(f"Step {cls._step}: Get sample (2 records) of {optional_post_ingestion_message}data:") cls._step = cls._step + 1 cls.execute_query(kusto_client, database_name, f"{table_name} | take 2") def main(): print("Kusto sample app is starting...") KustoSampleApp.start() print("Kusto sample app done") if __name__ == "__main__": main() azure-kusto-python-3.0.1/quick_start/kusto_sample_config.json000066400000000000000000000052561417222763000246160ustar00rootroot00000000000000{ "kustoUri" : "https://sdkse2etest.eastus.kusto.windows.net", "ingestUri" : "https://ingest-sdkse2etest.eastus.kusto.windows.net", "databaseName" : "e2e", "tableName" : "SampleTable", "useExistingTable": false, "alterTable": true, "queryData": true, "ingestData": true, "tableSchema" : "(rownumber:int, rowguid:string, xdouble:real, xfloat:real, xbool:bool, xint16:int, xint32:int, xint64:long, xuint8:long, xuint16:long, xuint32:long, xuint64:long, xdate:datetime, xsmalltext:string, xtext:string, xnumberAsText:string, xtime:timespan, xtextWithNulls:string, xdynamicWithNulls:dynamic)", "data" : [ { "sourceType": "localFileSource", "dataSourceUri": "dataset.csv", "format": "CSV", "useExistingMapping": true, "mappingName": "", "mappingValue": "" }, { "sourceType": "localFileSource", "dataSourceUri": "dataset.json", "format": "MULTIJSON", "useExistingMapping": false, "mappingName": "SampleTableMapping", "mappingValue": "[{\"Properties\":{\"Path\":\"$.rownumber\"},\"column\":\"rownumber\",\"datatype\":\"int\"}, {\"Properties\":{\"Path\":\"$.rowguid\"},\"column\":\"rowguid\",\"datatype\":\"string\"}, {\"Properties\":{\"Path\":\"$.xdouble\"},\"column\":\"xdouble\",\"datatype\":\"real\"}, {\"Properties\":{\"Path\":\"$.xfloat\"},\"column\":\"xfloat\",\"datatype\":\"real\"}, {\"Properties\":{\"Path\":\"$.xbool\"},\"column\":\"xbool\",\"datatype\":\"bool\"}, {\"Properties\":{\"Path\":\"$.xint16\"},\"column\":\"xint16\",\"datatype\":\"int\"}, {\"Properties\":{\"Path\":\"$.xint32\"},\"column\":\"xint32\",\"datatype\":\"int\"}, {\"Properties\":{\"Path\":\"$.xint64\"},\"column\":\"xint64\",\"datatype\":\"long\"}, {\"Properties\":{\"Path\":\"$.xuint8\"},\"column\":\"xuint8\",\"datatype\":\"long\"}, {\"Properties\":{\"Path\":\"$.xuint16\"},\"column\":\"xuint16\",\"datatype\":\"long\"}, {\"Properties\":{\"Path\":\"$.xuint32\"},\"column\":\"xuint32\",\"datatype\":\"long\"}, {\"Properties\":{\"Path\":\"$.xuint64\"},\"column\":\"xuint64\",\"datatype\":\"long\"}, {\"Properties\":{\"Path\":\"$.xdate\"},\"column\":\"xdate\",\"datatype\":\"datetime\"}, {\"Properties\":{\"Path\":\"$.xsmalltext\"},\"column\":\"xsmalltext\",\"datatype\":\"string\"}, {\"Properties\":{\"Path\":\"$.xtext\"},\"column\":\"xtext\",\"datatype\":\"string\"}, {\"Properties\":{\"Path\":\"$.rowguid\"},\"column\":\"xnumberAsText\",\"datatype\":\"string\"}, {\"Properties\":{\"Path\":\"$.xtime\"},\"column\":\"xtime\",\"datatype\":\"timespan\"}, {\"Properties\":{\"Path\":\"$.xtextWithNulls\"},\"column\":\"xtextWithNulls\",\"datatype\":\"string\"}, {\"Properties\":{\"Path\":\"$.xdynamicWithNulls\"},\"column\":\"xdynamicWithNulls\",\"datatype\":\"dynamic\"}]" } ] }azure-kusto-python-3.0.1/quick_start/oneclick_instruction_box.md000066400000000000000000000014531417222763000253050ustar00rootroot00000000000000### Prerequisites 1. Set up Python version 3.7 or higher on your machine. For instructions, consult a Python environment setup tutorial, like [this](https://www. tutorialspoint.com/java/java_environment_setup.htm). ### Instructions 1. Download the **DOWNLOAD_LINK** as a ZIP file. 1. Extract the app source code. 1. Open a command line window and navigate to the folder where you extracted the script. 1. Run `python -m pip install azure-kusto-data azure-kusto-ingest`. If upgrading, append ` --upgrade`. 1. Run `python kusto_sample_app.py`. It will already be configured to your cluster and source. ### Troubleshooting * If you are having trouble running the script from your IDE, first check if the script runs from command line, then consult the troubleshooting references of your IDE.azure-kusto-python-3.0.1/setup.cfg000066400000000000000000000002271417222763000171510ustar00rootroot00000000000000[bdist_wheel] universal=1 [flake8] ignore = E226,E302,E41 max-line-length = 160 exclude = tests/* max-complexity = 10 [pylint] max-line-length = 160azure-kusto-python-3.0.1/setup.py000066400000000000000000000003121417222763000170350ustar00rootroot00000000000000import sys if "travis_deploy" in sys.argv: import build_packages sys.exit(build_packages.travis_build_package()) else: raise ValueError("Setup file is written to support travis publish.") azure-kusto-python-3.0.1/test.bat000066400000000000000000000004051417222763000167750ustar00rootroot00000000000000cd %PROJECTS_HOME%\azure-kusto-python call workon kusto call pip uninstall azure-kusto-data azure-kusto-ingest -y call pip install ./azure-kusto-data[pandas] ./azure-kusto-ingest[pandas] call pip install --force-reinstall azure-nspkg==2.0.0 call pytest pause