MindsDB 25.3.1.0__py3-none-any.whl → 25.3.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of MindsDB might be problematic. Click here for more details.
- mindsdb/__about__.py +1 -1
- mindsdb/__main__.py +1 -2
- mindsdb/api/executor/sql_query/steps/union_step.py +21 -24
- mindsdb/api/http/gui.py +5 -4
- mindsdb/api/http/initialize.py +19 -19
- mindsdb/integrations/handlers/github_handler/generate_api.py +228 -0
- mindsdb/integrations/handlers/github_handler/github_handler.py +15 -8
- mindsdb/integrations/handlers/github_handler/requirements.txt +1 -1
- mindsdb/integrations/handlers/jira_handler/__init__.py +1 -0
- mindsdb/integrations/handlers/jira_handler/jira_handler.py +22 -80
- mindsdb/integrations/handlers/pgvector_handler/pgvector_handler.py +3 -3
- mindsdb/integrations/handlers/redshift_handler/redshift_handler.py +1 -0
- mindsdb/integrations/handlers/salesforce_handler/requirements.txt +1 -1
- mindsdb/integrations/handlers/salesforce_handler/salesforce_handler.py +20 -25
- mindsdb/integrations/handlers/salesforce_handler/salesforce_tables.py +2 -2
- mindsdb/integrations/handlers/slack_handler/slack_handler.py +2 -1
- mindsdb/integrations/handlers/timescaledb_handler/timescaledb_handler.py +11 -6
- mindsdb/integrations/libs/api_handler_generator.py +583 -0
- mindsdb/integrations/libs/ml_handler_process/learn_process.py +9 -3
- mindsdb/utilities/config.py +1 -1
- mindsdb/utilities/render/sqlalchemy_render.py +11 -5
- {mindsdb-25.3.1.0.dist-info → mindsdb-25.3.3.0.dist-info}/METADATA +219 -220
- {mindsdb-25.3.1.0.dist-info → mindsdb-25.3.3.0.dist-info}/RECORD +26 -28
- {mindsdb-25.3.1.0.dist-info → mindsdb-25.3.3.0.dist-info}/WHEEL +1 -1
- mindsdb/integrations/handlers/jira_handler/jira_table.py +0 -172
- mindsdb/integrations/handlers/jira_handler/requirements.txt +0 -1
- mindsdb/integrations/handlers/timescaledb_handler/tests/__init__.py +0 -0
- mindsdb/integrations/handlers/timescaledb_handler/tests/test_timescaledb_handler.py +0 -47
- {mindsdb-25.3.1.0.dist-info → mindsdb-25.3.3.0.dist-info}/LICENSE +0 -0
- {mindsdb-25.3.1.0.dist-info → mindsdb-25.3.3.0.dist-info}/top_level.txt +0 -0
mindsdb/__about__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
__title__ = 'MindsDB'
|
|
2
2
|
__package_name__ = 'mindsdb'
|
|
3
|
-
__version__ = '25.3.
|
|
3
|
+
__version__ = '25.3.3.0'
|
|
4
4
|
__description__ = "MindsDB's AI SQL Server enables developers to build AI tools that need access to real-time data to perform their tasks"
|
|
5
5
|
__email__ = "jorge@mindsdb.com"
|
|
6
6
|
__author__ = 'MindsDB Inc'
|
mindsdb/__main__.py
CHANGED
|
@@ -9,10 +9,10 @@ import secrets
|
|
|
9
9
|
import traceback
|
|
10
10
|
import threading
|
|
11
11
|
from enum import Enum
|
|
12
|
-
from packaging import version
|
|
13
12
|
from dataclasses import dataclass, field
|
|
14
13
|
from typing import Callable, Optional, Tuple, List
|
|
15
14
|
|
|
15
|
+
from packaging import version
|
|
16
16
|
from sqlalchemy.orm.attributes import flag_modified
|
|
17
17
|
|
|
18
18
|
from mindsdb.utilities import log
|
|
@@ -46,7 +46,6 @@ try:
|
|
|
46
46
|
except RuntimeError:
|
|
47
47
|
logger.info('Torch multiprocessing context already set, ignoring...')
|
|
48
48
|
|
|
49
|
-
|
|
50
49
|
_stop_event = threading.Event()
|
|
51
50
|
|
|
52
51
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import hashlib
|
|
2
|
-
|
|
3
1
|
from mindsdb.api.executor.planner.steps import UnionStep
|
|
4
2
|
|
|
5
3
|
from mindsdb.api.executor.sql_query.result_set import ResultSet
|
|
6
4
|
from mindsdb.api.executor.exceptions import WrongArgumentError
|
|
5
|
+
from mindsdb.api.executor.utilities.sql import query_df_with_type_infer_fallback
|
|
6
|
+
import numpy as np
|
|
7
7
|
|
|
8
8
|
from .base import BaseStepCall
|
|
9
9
|
|
|
@@ -30,27 +30,24 @@ class UnionStepCall(BaseStepCall):
|
|
|
30
30
|
# if type1 != type2:
|
|
31
31
|
# raise ErSqlWrongArguments(f'UNION types mismatch: {type1} != {type2}')
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
records_hashes.append(checksum)
|
|
53
|
-
results.append(list(row))
|
|
54
|
-
data.add_raw_values(results)
|
|
33
|
+
table_a, names = left_result.to_df_cols()
|
|
34
|
+
table_b, _ = right_result.to_df_cols()
|
|
35
|
+
|
|
36
|
+
op = 'UNION ALL'
|
|
37
|
+
if step.unique:
|
|
38
|
+
op = 'UNION'
|
|
39
|
+
query = f"""
|
|
40
|
+
SELECT * FROM table_a
|
|
41
|
+
{op}
|
|
42
|
+
SELECT * FROM table_b
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
resp_df, _description = query_df_with_type_infer_fallback(query, {
|
|
46
|
+
'table_a': table_a,
|
|
47
|
+
'table_b': table_b
|
|
48
|
+
})
|
|
49
|
+
resp_df.replace({np.nan: None}, inplace=True)
|
|
50
|
+
|
|
51
|
+
data = ResultSet().from_df_cols(resp_df, col_names=names)
|
|
55
52
|
|
|
56
53
|
return data
|
mindsdb/api/http/gui.py
CHANGED
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from zipfile import ZipFile
|
|
6
6
|
|
|
7
7
|
import requests
|
|
8
|
+
from packaging.version import Version
|
|
8
9
|
|
|
9
10
|
from mindsdb.utilities.config import Config
|
|
10
11
|
from mindsdb.utilities import log
|
|
@@ -65,7 +66,7 @@ def download_gui(destignation, version):
|
|
|
65
66
|
"""
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
def update_static(
|
|
69
|
+
def update_static(gui_version: Version):
|
|
69
70
|
"""Update Scout files basing on compatible-config.json content.
|
|
70
71
|
Files will be downloaded and updated if new version of GUI > current.
|
|
71
72
|
Current GUI version stored in static/version.txt.
|
|
@@ -74,11 +75,11 @@ def update_static(gui_version_lv):
|
|
|
74
75
|
static_path = Path(config["paths"]["static"])
|
|
75
76
|
|
|
76
77
|
logger.info(
|
|
77
|
-
f"New version of GUI available ({
|
|
78
|
+
f"New version of GUI available ({gui_version.base_version}). Downloading..."
|
|
78
79
|
)
|
|
79
80
|
|
|
80
81
|
temp_dir = tempfile.mkdtemp(prefix="mindsdb_gui_files_")
|
|
81
|
-
success = download_gui(temp_dir,
|
|
82
|
+
success = download_gui(temp_dir, gui_version.base_version)
|
|
82
83
|
if success is False:
|
|
83
84
|
shutil.rmtree(temp_dir)
|
|
84
85
|
return False
|
|
@@ -90,5 +91,5 @@ def update_static(gui_version_lv):
|
|
|
90
91
|
shutil.copytree(temp_dir, str(static_path))
|
|
91
92
|
shutil.rmtree(temp_dir_for_rm)
|
|
92
93
|
|
|
93
|
-
logger.info(f"GUI version updated to {
|
|
94
|
+
logger.info(f"GUI version updated to {gui_version.base_version}")
|
|
94
95
|
return True
|
mindsdb/api/http/initialize.py
CHANGED
|
@@ -6,7 +6,6 @@ import traceback
|
|
|
6
6
|
import webbrowser
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from http import HTTPStatus
|
|
9
|
-
from distutils.version import LooseVersion
|
|
10
9
|
|
|
11
10
|
import requests
|
|
12
11
|
from flask import Flask, url_for, make_response, request, send_from_directory
|
|
@@ -14,6 +13,7 @@ from flask.json import dumps
|
|
|
14
13
|
from flask_compress import Compress
|
|
15
14
|
from flask_restx import Api
|
|
16
15
|
from werkzeug.exceptions import HTTPException
|
|
16
|
+
from packaging.version import Version, parse as parse_version
|
|
17
17
|
|
|
18
18
|
from mindsdb.__about__ import __version__ as mindsdb_version
|
|
19
19
|
from mindsdb.api.http.gui import update_static
|
|
@@ -72,7 +72,7 @@ def custom_output_json(data, code, headers=None):
|
|
|
72
72
|
return resp
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
def get_last_compatible_gui_version() ->
|
|
75
|
+
def get_last_compatible_gui_version() -> Version:
|
|
76
76
|
logger.debug("Getting last compatible frontend..")
|
|
77
77
|
try:
|
|
78
78
|
res = requests.get('https://mindsdb-web-builds.s3.amazonaws.com/compatible-config.json', timeout=5)
|
|
@@ -95,7 +95,7 @@ def get_last_compatible_gui_version() -> LooseVersion:
|
|
|
95
95
|
logger.error(f"Cant decode compatible-config.json: {e}")
|
|
96
96
|
return False
|
|
97
97
|
|
|
98
|
-
current_mindsdb_lv =
|
|
98
|
+
current_mindsdb_lv = parse_version(mindsdb_version)
|
|
99
99
|
|
|
100
100
|
try:
|
|
101
101
|
gui_versions = {}
|
|
@@ -103,31 +103,31 @@ def get_last_compatible_gui_version() -> LooseVersion:
|
|
|
103
103
|
max_gui_lv = None
|
|
104
104
|
for el in versions['mindsdb']:
|
|
105
105
|
if el['mindsdb_version'] is None:
|
|
106
|
-
gui_lv =
|
|
106
|
+
gui_lv = parse_version(el['gui_version'])
|
|
107
107
|
else:
|
|
108
|
-
mindsdb_lv =
|
|
109
|
-
gui_lv =
|
|
110
|
-
if mindsdb_lv.
|
|
111
|
-
gui_versions[mindsdb_lv.
|
|
108
|
+
mindsdb_lv = parse_version(el['mindsdb_version'])
|
|
109
|
+
gui_lv = parse_version(el['gui_version'])
|
|
110
|
+
if mindsdb_lv.base_version not in gui_versions or gui_lv > gui_versions[mindsdb_lv.base_version]:
|
|
111
|
+
gui_versions[mindsdb_lv.base_version] = gui_lv
|
|
112
112
|
if max_mindsdb_lv is None or max_mindsdb_lv < mindsdb_lv:
|
|
113
113
|
max_mindsdb_lv = mindsdb_lv
|
|
114
114
|
if max_gui_lv is None or max_gui_lv < gui_lv:
|
|
115
115
|
max_gui_lv = gui_lv
|
|
116
116
|
|
|
117
|
-
all_mindsdb_lv = [
|
|
117
|
+
all_mindsdb_lv = [parse_version(x) for x in gui_versions.keys()]
|
|
118
118
|
all_mindsdb_lv.sort()
|
|
119
119
|
|
|
120
|
-
if current_mindsdb_lv.
|
|
121
|
-
gui_version_lv = gui_versions[current_mindsdb_lv.
|
|
120
|
+
if current_mindsdb_lv.base_version in gui_versions:
|
|
121
|
+
gui_version_lv = gui_versions[current_mindsdb_lv.base_version]
|
|
122
122
|
elif current_mindsdb_lv > all_mindsdb_lv[-1]:
|
|
123
123
|
gui_version_lv = max_gui_lv
|
|
124
124
|
else:
|
|
125
|
-
lower_versions = {key: value for key, value in gui_versions.items() if
|
|
125
|
+
lower_versions = {key: value for key, value in gui_versions.items() if parse_version(key) < current_mindsdb_lv}
|
|
126
126
|
if len(lower_versions) == 0:
|
|
127
|
-
gui_version_lv = gui_versions[all_mindsdb_lv[0].
|
|
127
|
+
gui_version_lv = gui_versions[all_mindsdb_lv[0].base_version]
|
|
128
128
|
else:
|
|
129
|
-
all_lower_versions = [
|
|
130
|
-
gui_version_lv = gui_versions[all_lower_versions[-1].
|
|
129
|
+
all_lower_versions = [parse_version(x) for x in lower_versions.keys()]
|
|
130
|
+
gui_version_lv = gui_versions[all_lower_versions[-1].base_version]
|
|
131
131
|
except Exception as e:
|
|
132
132
|
logger.error(f"Error in compatible-config.json structure: {e}")
|
|
133
133
|
return False
|
|
@@ -136,7 +136,7 @@ def get_last_compatible_gui_version() -> LooseVersion:
|
|
|
136
136
|
return gui_version_lv
|
|
137
137
|
|
|
138
138
|
|
|
139
|
-
def get_current_gui_version() ->
|
|
139
|
+
def get_current_gui_version() -> Version:
|
|
140
140
|
logger.debug("Getting current frontend version..")
|
|
141
141
|
config = Config()
|
|
142
142
|
static_path = Path(config['paths']['static'])
|
|
@@ -148,7 +148,7 @@ def get_current_gui_version() -> LooseVersion:
|
|
|
148
148
|
current_gui_version = f.readline()
|
|
149
149
|
|
|
150
150
|
current_gui_lv = (
|
|
151
|
-
None if current_gui_version is None else
|
|
151
|
+
None if current_gui_version is None else parse_version(current_gui_version)
|
|
152
152
|
)
|
|
153
153
|
logger.debug(f"Current frontend version: {current_gui_lv}.")
|
|
154
154
|
|
|
@@ -163,7 +163,7 @@ def initialize_static():
|
|
|
163
163
|
required_gui_version = config['gui'].get('version')
|
|
164
164
|
|
|
165
165
|
if required_gui_version is not None:
|
|
166
|
-
required_gui_version_lv =
|
|
166
|
+
required_gui_version_lv = parse_version(required_gui_version)
|
|
167
167
|
success = True
|
|
168
168
|
if (
|
|
169
169
|
current_gui_version_lv is None
|
|
@@ -176,7 +176,7 @@ def initialize_static():
|
|
|
176
176
|
return False
|
|
177
177
|
|
|
178
178
|
# ignore versions like '23.9.2.2'
|
|
179
|
-
if current_gui_version_lv is not None and len(current_gui_version_lv.
|
|
179
|
+
if current_gui_version_lv is not None and len(current_gui_version_lv.release) < 3:
|
|
180
180
|
if current_gui_version_lv == last_gui_version_lv:
|
|
181
181
|
return True
|
|
182
182
|
logger.debug("Updating gui..")
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import List
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import github
|
|
7
|
+
|
|
8
|
+
from mindsdb.integrations.utilities.sql_utils import (FilterCondition, FilterOperator, SortColumn)
|
|
9
|
+
from mindsdb.integrations.libs.api_handler import APIResource
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Type:
|
|
14
|
+
name: str
|
|
15
|
+
sub_type: str = None
|
|
16
|
+
optional: bool = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class GHMethod:
|
|
21
|
+
name: str
|
|
22
|
+
table_name: str
|
|
23
|
+
params: dict
|
|
24
|
+
output: Type
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_annotations(annotations):
|
|
28
|
+
'''
|
|
29
|
+
Parse string annotation, and extract type, input examples:
|
|
30
|
+
- Milestone | Opt[str]
|
|
31
|
+
- PaginatedList[Issue]
|
|
32
|
+
'''
|
|
33
|
+
type_name, sub_type = None, None
|
|
34
|
+
if not isinstance(annotations, str):
|
|
35
|
+
|
|
36
|
+
return Type(getattr(annotations, '__name__', None))
|
|
37
|
+
for item in annotations.split('|'):
|
|
38
|
+
item = item.strip()
|
|
39
|
+
if item is None:
|
|
40
|
+
continue
|
|
41
|
+
if '[' in item:
|
|
42
|
+
type_name = item[: item.find('[')]
|
|
43
|
+
item2 = item[item.find('[') + 1: item.rfind(']')]
|
|
44
|
+
if type_name == 'Opt':
|
|
45
|
+
inner_type = parse_annotations(item2)
|
|
46
|
+
inner_type.optional = Type
|
|
47
|
+
return inner_type
|
|
48
|
+
if type_name == 'dict':
|
|
49
|
+
item2 = item2[item2.find(',') + 1:]
|
|
50
|
+
sub_type = parse_annotations(item2).name
|
|
51
|
+
else:
|
|
52
|
+
type_name = item
|
|
53
|
+
# get only first type
|
|
54
|
+
break
|
|
55
|
+
return Type(type_name, sub_type)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_properties(cls):
|
|
59
|
+
# find properties of the class
|
|
60
|
+
|
|
61
|
+
properties = {}
|
|
62
|
+
for prop_name, prop in inspect.getmembers(cls):
|
|
63
|
+
if prop_name.startswith('_'):
|
|
64
|
+
continue
|
|
65
|
+
if not isinstance(prop, property):
|
|
66
|
+
continue
|
|
67
|
+
sig2 = inspect.signature(prop.fget)
|
|
68
|
+
|
|
69
|
+
properties[prop_name] = parse_annotations(sig2.return_annotation)
|
|
70
|
+
return properties
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_github_types():
|
|
74
|
+
# get github types
|
|
75
|
+
types = {}
|
|
76
|
+
|
|
77
|
+
GithubObject = github.GithubObject.GithubObject
|
|
78
|
+
for module_name, module in inspect.getmembers(github, inspect.ismodule):
|
|
79
|
+
cls = getattr(module, module_name, None)
|
|
80
|
+
if cls is None:
|
|
81
|
+
continue
|
|
82
|
+
if issubclass(cls, GithubObject):
|
|
83
|
+
|
|
84
|
+
# remove inherited props
|
|
85
|
+
parent_props = []
|
|
86
|
+
for cls2 in cls.__bases__:
|
|
87
|
+
parent_props += get_properties(cls2).keys()
|
|
88
|
+
|
|
89
|
+
properties = {}
|
|
90
|
+
for k, v in get_properties(cls).items():
|
|
91
|
+
if k not in parent_props:
|
|
92
|
+
properties[k] = v
|
|
93
|
+
|
|
94
|
+
types[module_name] = properties
|
|
95
|
+
return types
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_github_methods(cls):
|
|
99
|
+
'''
|
|
100
|
+
Analyse class in order to find methods which return list of objects.
|
|
101
|
+
'''
|
|
102
|
+
methods = []
|
|
103
|
+
|
|
104
|
+
for method_name, method in inspect.getmembers(cls, inspect.isfunction):
|
|
105
|
+
sig = inspect.signature(method)
|
|
106
|
+
|
|
107
|
+
return_type = parse_annotations(sig.return_annotation)
|
|
108
|
+
list_prefix = 'get_'
|
|
109
|
+
if not (method_name.startswith(list_prefix) and return_type.name == 'PaginatedList'):
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
table_name = method_name[len(list_prefix):]
|
|
113
|
+
|
|
114
|
+
params = {}
|
|
115
|
+
for param_name, param in sig.parameters.items():
|
|
116
|
+
params[param_name] = parse_annotations(param.annotation)
|
|
117
|
+
|
|
118
|
+
methods.append(GHMethod(
|
|
119
|
+
name=method_name,
|
|
120
|
+
table_name=table_name,
|
|
121
|
+
params=params,
|
|
122
|
+
output=return_type
|
|
123
|
+
))
|
|
124
|
+
return methods
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class GHTable(APIResource):
|
|
128
|
+
def __init__(self, *args, method: GHMethod = None, github_types=None, **kwargs):
|
|
129
|
+
self.method = method
|
|
130
|
+
self.github_types = github_types
|
|
131
|
+
|
|
132
|
+
self.output_columns = {}
|
|
133
|
+
if method.output.sub_type in self.github_types:
|
|
134
|
+
self.output_columns = self.github_types[method.output.sub_type]
|
|
135
|
+
|
|
136
|
+
# check params:
|
|
137
|
+
self.params, self.list_params = [], []
|
|
138
|
+
for name, param_type in method.params.items():
|
|
139
|
+
self.params.append(name)
|
|
140
|
+
if param_type.name == 'list':
|
|
141
|
+
self.list_params.append(name)
|
|
142
|
+
|
|
143
|
+
self._allow_sort = 'sort' in method.params
|
|
144
|
+
|
|
145
|
+
super().__init__(*args, **kwargs)
|
|
146
|
+
|
|
147
|
+
def repr_value(self, value, type_name):
|
|
148
|
+
if value is None or type_name in ('bool', 'int', 'float'):
|
|
149
|
+
return value
|
|
150
|
+
if type_name in self.github_types:
|
|
151
|
+
properties = self.github_types[type_name]
|
|
152
|
+
if 'login' in properties:
|
|
153
|
+
value = getattr(value, 'login')
|
|
154
|
+
elif 'url' in properties:
|
|
155
|
+
value = getattr(value, 'url')
|
|
156
|
+
return str(value)
|
|
157
|
+
|
|
158
|
+
def get_columns(self) -> List[str]:
|
|
159
|
+
return list(self.output_columns.keys())
|
|
160
|
+
|
|
161
|
+
def list(
|
|
162
|
+
self,
|
|
163
|
+
conditions: List[FilterCondition] = None,
|
|
164
|
+
limit: int = None,
|
|
165
|
+
sort: List[SortColumn] = None,
|
|
166
|
+
targets: List[str] = None,
|
|
167
|
+
**kwargs
|
|
168
|
+
) -> pd.DataFrame:
|
|
169
|
+
|
|
170
|
+
if limit is None:
|
|
171
|
+
limit = 20
|
|
172
|
+
|
|
173
|
+
method_kwargs = {}
|
|
174
|
+
if sort is not None and self._allow_sort:
|
|
175
|
+
for col in sort:
|
|
176
|
+
method_kwargs['sort'] = col.column
|
|
177
|
+
method_kwargs['direction'] = 'asc' if col.ascending else 'desc'
|
|
178
|
+
sort.applied = True
|
|
179
|
+
# supported only 1 column
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
if conditions:
|
|
183
|
+
for condition in conditions:
|
|
184
|
+
if condition.column not in self.params:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if condition.column in self.list_params:
|
|
188
|
+
if condition.op == FilterOperator.IN:
|
|
189
|
+
method_kwargs[condition.column] = condition.value
|
|
190
|
+
elif condition.op == FilterOperator.EQUAL:
|
|
191
|
+
method_kwargs[condition.column] = [condition]
|
|
192
|
+
condition.applied = True
|
|
193
|
+
else:
|
|
194
|
+
method_kwargs[condition.column] = condition.value
|
|
195
|
+
condition.applied = True
|
|
196
|
+
|
|
197
|
+
connection = self.handler.connect()
|
|
198
|
+
method = getattr(connection.get_repo(self.handler.repository), self.method.name)
|
|
199
|
+
|
|
200
|
+
data = []
|
|
201
|
+
count = 0
|
|
202
|
+
for record in method(**method_kwargs):
|
|
203
|
+
item = {}
|
|
204
|
+
for name, output_type in self.output_columns.items():
|
|
205
|
+
|
|
206
|
+
# workaround to prevent making addition request per property.
|
|
207
|
+
if name in targets:
|
|
208
|
+
# request only if is required
|
|
209
|
+
value = getattr(record, name)
|
|
210
|
+
else:
|
|
211
|
+
value = getattr(record, '_' + name).value
|
|
212
|
+
if value is not None:
|
|
213
|
+
if output_type.name == 'list':
|
|
214
|
+
value = ",".join([
|
|
215
|
+
str(self.repr_value(i, output_type.sub_type))
|
|
216
|
+
for i in value
|
|
217
|
+
])
|
|
218
|
+
else:
|
|
219
|
+
value = self.repr_value(value, output_type.name)
|
|
220
|
+
item[name] = value
|
|
221
|
+
|
|
222
|
+
data.append(item)
|
|
223
|
+
|
|
224
|
+
count += 1
|
|
225
|
+
if limit <= count:
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
return pd.DataFrame(data, columns=self.get_columns())
|
|
@@ -12,7 +12,7 @@ from mindsdb.integrations.handlers.github_handler.github_tables import (
|
|
|
12
12
|
GithubMilestonesTable,
|
|
13
13
|
GithubProjectsTable, GithubFilesTable
|
|
14
14
|
)
|
|
15
|
-
|
|
15
|
+
from mindsdb.integrations.handlers.github_handler.generate_api import get_github_types, get_github_methods, GHTable
|
|
16
16
|
from mindsdb.integrations.libs.api_handler import APIHandler
|
|
17
17
|
from mindsdb.integrations.libs.response import (
|
|
18
18
|
HandlerStatusResponse as StatusResponse,
|
|
@@ -22,6 +22,7 @@ from mindsdb.utilities import log
|
|
|
22
22
|
|
|
23
23
|
logger = log.getLogger(__name__)
|
|
24
24
|
|
|
25
|
+
|
|
25
26
|
class GithubHandler(APIHandler):
|
|
26
27
|
"""The GitHub handler implementation"""
|
|
27
28
|
|
|
@@ -43,16 +44,22 @@ class GithubHandler(APIHandler):
|
|
|
43
44
|
self.connection = None
|
|
44
45
|
self.is_connected = False
|
|
45
46
|
|
|
47
|
+
# custom tables
|
|
46
48
|
self._register_table("issues", GithubIssuesTable(self))
|
|
47
|
-
self._register_table("pull_requests", GithubPullRequestsTable(self))
|
|
48
|
-
self._register_table("commits", GithubCommitsTable(self))
|
|
49
|
-
self._register_table("releases", GithubReleasesTable(self))
|
|
50
|
-
self._register_table("branches", GithubBranchesTable(self))
|
|
51
|
-
self._register_table("contributors", GithubContributorsTable(self))
|
|
52
|
-
self._register_table("milestones", GithubMilestonesTable(self))
|
|
53
|
-
self._register_table("projects", GithubProjectsTable(self))
|
|
54
49
|
self._register_table("files", GithubFilesTable(self))
|
|
55
50
|
|
|
51
|
+
# generated tables
|
|
52
|
+
github_types = get_github_types()
|
|
53
|
+
|
|
54
|
+
# generate tables from repository object
|
|
55
|
+
for method in get_github_methods(github.Repository.Repository):
|
|
56
|
+
if method.table_name in self._tables:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
table = GHTable(self, github_types=github_types, method=method)
|
|
60
|
+
self._register_table(method.table_name, table)
|
|
61
|
+
|
|
62
|
+
|
|
56
63
|
def connect(self) -> StatusResponse:
|
|
57
64
|
"""Set up the connection required by the handler.
|
|
58
65
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
pygithub
|
|
1
|
+
pygithub==2.6.1
|
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
from mindsdb.integrations.handlers.jira_handler.jira_table import JiraProjectsTable
|
|
2
1
|
from mindsdb.integrations.libs.api_handler import APIHandler
|
|
3
2
|
from mindsdb.integrations.libs.response import (
|
|
4
3
|
HandlerStatusResponse as StatusResponse,
|
|
5
4
|
)
|
|
6
5
|
from mindsdb.utilities import log
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
from atlassian import Jira
|
|
10
|
-
from typing import Optional
|
|
11
|
-
import requests
|
|
6
|
+
from mindsdb.integrations.libs.api_handler_generator import APIResourceGenerator
|
|
12
7
|
|
|
13
8
|
|
|
14
9
|
logger = log.getLogger(__name__)
|
|
15
10
|
|
|
16
|
-
class JiraHandler(APIHandler):
|
|
17
|
-
"""
|
|
18
|
-
This handler handles connection and execution of the Airtable statements.
|
|
19
|
-
"""
|
|
20
11
|
|
|
12
|
+
class JiraHandler(APIHandler):
|
|
21
13
|
|
|
22
14
|
def __init__(self, name=None, **kwargs):
|
|
23
15
|
"""
|
|
@@ -30,64 +22,38 @@ class JiraHandler(APIHandler):
|
|
|
30
22
|
super().__init__(name)
|
|
31
23
|
self.connection_data = kwargs.get("connection_data", {})
|
|
32
24
|
|
|
33
|
-
self.parser = parse_sql
|
|
34
|
-
self.dialect = 'jira'
|
|
35
|
-
self.kwargs = kwargs
|
|
36
25
|
self.connection = None
|
|
37
26
|
self.is_connected = False
|
|
38
27
|
|
|
28
|
+
# todo store parsed data in files
|
|
29
|
+
|
|
30
|
+
self.api_resource_generator = APIResourceGenerator(
|
|
31
|
+
"https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json",
|
|
32
|
+
self.connection_data,
|
|
33
|
+
url_base='/rest/api/3/',
|
|
34
|
+
options={
|
|
35
|
+
'offset_param': ['startAt', 'offset'],
|
|
36
|
+
'total_column': ['totalEntryCount', 'total'],
|
|
37
|
+
'check_connection_table': 'myself'
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
resource_tables = self.api_resource_generator.generate_api_resources(self)
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
for table_name, resource in resource_tables.items():
|
|
44
|
+
self._register_table(table_name, resource)
|
|
42
45
|
|
|
43
46
|
def __del__(self):
|
|
44
47
|
if self.is_connected is True:
|
|
45
48
|
self.disconnect()
|
|
46
49
|
|
|
47
|
-
def connect(self)
|
|
50
|
+
def connect(self):
|
|
48
51
|
"""
|
|
49
52
|
Set up the connection required by the handler.
|
|
50
53
|
Returns:
|
|
51
54
|
HandlerStatusResponse
|
|
52
55
|
"""
|
|
53
|
-
|
|
54
|
-
if self.is_connected is True:
|
|
55
|
-
return self.connection
|
|
56
|
-
|
|
57
|
-
s = requests.Session()
|
|
58
|
-
if self.connection_data.get("cloud", False):
|
|
59
|
-
params = {
|
|
60
|
-
"cloud": True,
|
|
61
|
-
"username": self.connection_data['jira_username'],
|
|
62
|
-
"password": self.connection_data['jira_api_token'],
|
|
63
|
-
"url": self.connection_data['jira_url'],
|
|
64
|
-
}
|
|
65
|
-
else:
|
|
66
|
-
params = {
|
|
67
|
-
"cloud": False,
|
|
68
|
-
"url": self.connection_data['jira_url'],
|
|
69
|
-
"session": s
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
s.headers['Authorization'] = f"Bearer {self.connection_data['jira_api_token']}"
|
|
73
|
-
|
|
74
|
-
self.connection = Jira(**params)
|
|
75
|
-
self.is_connected = True
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return self.connection
|
|
79
|
-
|
|
80
|
-
def disconnect(self):
|
|
81
|
-
"""
|
|
82
|
-
Close any existing connections.
|
|
83
|
-
"""
|
|
84
|
-
|
|
85
|
-
if self.is_connected is False:
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
self.connection.close()
|
|
89
|
-
self.is_connected = False
|
|
90
|
-
return self.is_connected
|
|
56
|
+
return
|
|
91
57
|
|
|
92
58
|
def check_connection(self) -> StatusResponse:
|
|
93
59
|
"""
|
|
@@ -97,37 +63,13 @@ class JiraHandler(APIHandler):
|
|
|
97
63
|
"""
|
|
98
64
|
|
|
99
65
|
response = StatusResponse(False)
|
|
100
|
-
|
|
101
|
-
|
|
66
|
+
|
|
102
67
|
try:
|
|
103
|
-
self.
|
|
68
|
+
self.api_resource_generator.check_connection()
|
|
104
69
|
response.success = True
|
|
105
70
|
except Exception as e:
|
|
106
71
|
logger.error(f"Error connecting to Jira API: {e}!")
|
|
107
72
|
response.error_message = e
|
|
108
73
|
|
|
109
74
|
self.is_connected = response.success
|
|
110
|
-
|
|
111
75
|
return response
|
|
112
|
-
|
|
113
|
-
def native_query(self, query: str) -> StatusResponse:
|
|
114
|
-
"""Receive and process a raw query.
|
|
115
|
-
Parameters
|
|
116
|
-
----------
|
|
117
|
-
query : str
|
|
118
|
-
query in a native format
|
|
119
|
-
Returns
|
|
120
|
-
-------
|
|
121
|
-
StatusResponse
|
|
122
|
-
Request status
|
|
123
|
-
"""
|
|
124
|
-
ast = parse_sql(query)
|
|
125
|
-
return self.query(ast)
|
|
126
|
-
|
|
127
|
-
def construct_jql(self):
|
|
128
|
-
"""construct jql & returns it to JiraProjectsTable class
|
|
129
|
-
Returns
|
|
130
|
-
-------
|
|
131
|
-
Str
|
|
132
|
-
"""
|
|
133
|
-
return 'project = ' + str(self.connection_data['project'])
|