-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Looker SDK generator for the Dart language #933
base: main
Are you sure you want to change the base?
Changes from all commits
6e70e89
83309aa
6d68297
108aeef
c0028a0
35f1b83
a04503b
9f0a8f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# the dart lib needs to be included in source control | ||
!lib/** | ||
|
||
# the following are excluded from source control. | ||
.env* | ||
temp/ | ||
.dart_tool/ | ||
.packages | ||
build/ | ||
# recommendation is NOT to commit for library packages. | ||
pubspec.lock | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# Looker API for Dart SDK | ||
|
||
A dart implementation of the Looker API. Note that only the SDK for Looker 4.0 API is generated. | ||
|
||
## Usage | ||
|
||
See examples and tests. | ||
|
||
Create a `.env` file in the same directory as this `README.md`. The format is as follows: | ||
|
||
``` | ||
URL=looker_instance_api_endpoint | ||
CLIENT_ID=client_id_from_looker_instance | ||
CLIENT_SECRET=client_secret_from_looker_instance | ||
``` | ||
|
||
## Add to project | ||
|
||
Add the following to project `pubspec.yaml` dependencies. Replace `{SHA}` with the sha of the version of the SDK you want to use (a more permanent solution may be added in the future). | ||
|
||
``` | ||
looker_sdk: | ||
git: | ||
url: https://github.com/looker-open-source/sdk-codegen | ||
ref: {SHA} | ||
path: dart/looker_sdk | ||
``` | ||
|
||
## Developing | ||
|
||
Relies on `yarn` and `dart` being installed. This was developed with `dart` version `2.15.1` so the recommendation is to have a version of dart that is at least at that version. | ||
|
||
### Generate | ||
|
||
Run `yarn sdk Gen` from the `{reporoot}`. Note that the SDK generator needs to be built using `yarn build`. If changing the generator run 'yarn watch` in a separate window. This command generates two files: | ||
|
||
1. `{reporoot}/dart/looker_sdk/lib/src/sdk/methods.dart` | ||
2. `{reporoot}/dart/looker_sdk/lib/src/sdk/models.dart` | ||
|
||
The files are automatically formatted using `dart` tooling. Ensure that the `dart` binary is available on your path. | ||
|
||
### Run example | ||
|
||
Run `yarn example` from `{reporoot}/dart/looker_sdk` | ||
|
||
### Run tests | ||
|
||
Run `yarn test:e2e` from `{reporoot}/dart/looker_sdk` to run end to end tests. Note that these tests require that a `.env` file has been created (see above) and that the Looker instance is running. | ||
|
||
Run `yarn test:unit` from `{reporoot}/dart/looker_sdk` to run unit tests. These tests do not require a Looker instance to be running. | ||
|
||
Run `yarn test` from `{reporoot}/dart/looker_sdk` to run all tests. | ||
|
||
### Run format | ||
|
||
Run `yarn format` from `{reporoot}/dart/looker_sdk` to format the `dart` files correctly. This should be run if you change any of the run time library `dart` files. The repo CI will run the `format-check` and will fail if the files have not been correctly formatted. | ||
|
||
### Run format-check | ||
|
||
Run `yarn format-check` from `{reporoot}/dart/looker_sdk` to verify the formatting of the `dart` files. This is primarily for CI. It's the same as `yarn format` but does not format the files. | ||
|
||
### Run analyze | ||
|
||
Run `yarn format-analyze` from `{reporoot}/dart/looker_sdk` to lint the `dart` files. This should be run prior to commiting as CI will this task and will fail if the script fails. | ||
|
||
## TODOs | ||
|
||
1. Make enum mappers private to package. They are currently public as some enums are not used by by the models and a warning for unused class is displayed by visual code. It could also be a bug in either the generator or the spec generator (why are enums being generated if they are not being used?). | ||
2. Add optional timeout parameter to methods and implement timeout support. | ||
3. Add additional authorization methods to api keys. | ||
4. Revisit auth session. There is some duplication of code in generated methods. | ||
5. Add base class for models. Move common props to base class. Maybe add some utility methods for primitive types. Should reduce size of models.dart file. | ||
6. More and better generator tests. They are a bit hacky at that moment. | ||
7. Generate dart documentation. | ||
|
||
## Notes | ||
|
||
1. Region folding: Dart does not currently support region folding. Visual Studio Code has a generic extension that supports region folding for dart. [Install](https://marketplace.visualstudio.com/items?itemName=maptz.regionfolder) if you wish the generated regions to be honored. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
include: package:lints/recommended.yaml | ||
|
||
analyzer: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import 'dart:io'; | ||
import 'dart:typed_data'; | ||
import 'package:looker_sdk/index.dart'; | ||
import 'package:dotenv/dotenv.dart' show load, env; | ||
|
||
void main() async { | ||
load(); | ||
var sdk = await createSdk(); | ||
await runLooks(sdk); | ||
await runDashboardApis(sdk); | ||
await runConnectionApis(sdk); | ||
} | ||
Comment on lines
+6
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very handy ref. Thanks! |
||
|
||
Future<LookerSDK> createSdk() async { | ||
return await Sdk.createSdk({ | ||
'base_url': env['URL'], | ||
'verify_ssl': false, | ||
'credentials_callback': credentialsCallback | ||
}); | ||
} | ||
|
||
Future<void> runLooks(LookerSDK sdk) async { | ||
try { | ||
var looks = await sdk.ok(sdk.allLooks()); | ||
if (looks.isNotEmpty) { | ||
for (var look in looks) { | ||
print(look.title); | ||
} | ||
var look = await sdk.ok(sdk.runLook(looks[looks.length - 1].id, 'png')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW if you want to have a test for binary payloads, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea. Let me change it. This was an endpoint I knew about but it is slow! |
||
var dir = Directory('./temp'); | ||
if (!dir.existsSync()) { | ||
dir.createSync(); | ||
} | ||
File('./temp/look.png').writeAsBytesSync(look as Uint8List); | ||
look = await sdk.ok(sdk.runLook(looks[looks.length - 1].id, 'csv')); | ||
File('./temp/look.csv').writeAsStringSync(look as String); | ||
} | ||
} catch (error, stacktrace) { | ||
print(error); | ||
print(stacktrace); | ||
} | ||
} | ||
|
||
Future<void> runDashboardApis(LookerSDK sdk) async { | ||
try { | ||
var dashboards = await sdk.ok(sdk.allDashboards()); | ||
for (var dashboard in dashboards) { | ||
print(dashboard.title); | ||
} | ||
var dashboard = await sdk.ok(sdk.dashboard(dashboards[0].id)); | ||
print(dashboard.toJson()); | ||
} catch (error, stacktrace) { | ||
print(error); | ||
print(stacktrace); | ||
} | ||
} | ||
|
||
Future<void> runConnectionApis(LookerSDK sdk) async { | ||
try { | ||
var connections = await sdk.ok(sdk.allConnections()); | ||
for (var connection in connections) { | ||
print(connection.name); | ||
} | ||
var connection = await sdk | ||
.ok(sdk.connection(connections[0].name, fields: 'name,host,port')); | ||
print( | ||
'name=${connection.name} host=${connection.host} port=${connection.port}'); | ||
var newConnection = WriteDBConnection(); | ||
SDKResponse resp = await sdk.connection('TestConnection'); | ||
if (resp.statusCode == 200) { | ||
print('TestConnection already exists'); | ||
} else { | ||
newConnection.name = 'TestConnection'; | ||
newConnection.dialectName = 'mysql'; | ||
newConnection.host = 'db1.looker.com'; | ||
newConnection.port = '3306'; | ||
newConnection.username = 'looker_demoX'; | ||
newConnection.password = 'look_your_data'; | ||
newConnection.database = 'demo_db2'; | ||
newConnection.tmpDbName = 'looker_demo_scratch'; | ||
connection = await sdk.ok(sdk.createConnection(newConnection)); | ||
print('created ${connection.name}'); | ||
} | ||
var updateConnection = WriteDBConnection(); | ||
updateConnection.username = 'looker_demo'; | ||
connection = | ||
await sdk.ok(sdk.updateConnection('TestConnection', updateConnection)); | ||
print('Connection updated: username=${connection.username}'); | ||
var testResults = await sdk.ok( | ||
sdk.testConnection('TestConnection', tests: DelimList(['connect']))); | ||
if (testResults.isEmpty) { | ||
print('No connection tests run'); | ||
} else { | ||
for (var i in testResults) { | ||
print('test result: ${i.name}=${i.message}'); | ||
} | ||
} | ||
var deleteResult = await sdk.ok(sdk.deleteConnection('TestConnection')); | ||
print('Delete result $deleteResult'); | ||
} catch (error, stacktrace) { | ||
print(error); | ||
print(stacktrace); | ||
} | ||
} | ||
|
||
Map credentialsCallback() { | ||
return {'client_id': env['CLIENT_ID'], 'client_secret': env['CLIENT_SECRET']}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export 'src/looker_sdk.dart'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export 'rtl/api_types.dart'; | ||
export 'rtl/api_methods.dart'; | ||
export 'rtl/api_settings.dart'; | ||
export 'rtl/auth_session.dart'; | ||
export 'rtl/auth_token.dart'; | ||
export 'rtl/constants.dart'; | ||
export 'rtl/sdk.dart'; | ||
export 'rtl/transport.dart'; | ||
export 'sdk/methods.dart'; | ||
export 'sdk/models.dart'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import 'dart:convert'; | ||
import 'auth_session.dart'; | ||
import 'transport.dart'; | ||
|
||
class APIMethods { | ||
final AuthSession _authSession; | ||
|
||
APIMethods(this._authSession); | ||
|
||
Future<T> ok<T>(Future<SDKResponse<T>> future) async { | ||
var response = await future; | ||
if (response.ok) { | ||
return response.result; | ||
} else { | ||
throw Exception( | ||
'Invalid SDK response ${response.statusCode}/${response.statusText}/${response.decodedRawResult}'); | ||
} | ||
} | ||
|
||
Future<SDKResponse<T>> get<T>( | ||
T Function(dynamic responseData, String contentType) responseHandler, | ||
String path, | ||
[dynamic queryParams, | ||
dynamic body]) async { | ||
var headers = await _getHeaders(); | ||
return _authSession.transport.request( | ||
responseHandler, | ||
HttpMethod.get, | ||
'${_authSession.apiPath}$path', | ||
queryParams, | ||
body, | ||
headers, | ||
); | ||
} | ||
|
||
Future<SDKResponse> head(String path, | ||
[dynamic queryParams, dynamic body]) async { | ||
var headers = await _getHeaders(); | ||
dynamic responseHandler(dynamic responseData, String contentType) { | ||
return null; | ||
} | ||
|
||
return _authSession.transport.request(responseHandler, HttpMethod.head, | ||
'${_authSession.apiPath}$path', queryParams, body, headers); | ||
} | ||
|
||
Future<SDKResponse<T>> delete<T>( | ||
T Function(dynamic responseData, String contentType) responseHandler, | ||
String path, | ||
[dynamic queryParams, | ||
dynamic body]) async { | ||
var headers = await _getHeaders(); | ||
return _authSession.transport.request(responseHandler, HttpMethod.delete, | ||
'${_authSession.apiPath}$path', queryParams, body, headers); | ||
} | ||
|
||
Future<SDKResponse<T>> post<T>( | ||
T Function(dynamic responseData, String contentType) responseHandler, | ||
String path, | ||
[dynamic queryParams, | ||
dynamic body]) async { | ||
var headers = await _getHeaders(); | ||
var requestBody = body == null ? null : jsonEncode(body); | ||
return _authSession.transport.request(responseHandler, HttpMethod.post, | ||
'${_authSession.apiPath}$path', queryParams, requestBody, headers); | ||
} | ||
|
||
Future<SDKResponse<T>> put<T>( | ||
T Function(dynamic responseData, String contentType) responseHandler, | ||
String path, | ||
[dynamic queryParams, | ||
dynamic body]) async { | ||
var headers = await _getHeaders(); | ||
return _authSession.transport.request(responseHandler, HttpMethod.put, | ||
'${_authSession.apiPath}$path', queryParams, body, headers); | ||
} | ||
|
||
Future<SDKResponse<T>> patch<T>( | ||
T Function(dynamic responseData, String contentType) responseHandler, | ||
String path, | ||
[dynamic queryParams, | ||
dynamic body]) async { | ||
var headers = await _getHeaders(); | ||
Object requestBody; | ||
if (body != null) { | ||
body.removeWhere((key, value) => value == null); | ||
requestBody = jsonEncode(body); | ||
} | ||
return _authSession.transport.request(responseHandler, HttpMethod.patch, | ||
'${_authSession.apiPath}$path', queryParams, requestBody, headers); | ||
} | ||
|
||
Future<Map<String, String>> _getHeaders() async { | ||
var headers = <String, String>{ | ||
'x-looker-appid': _authSession.transport.settings.agentTag | ||
}; | ||
headers.addAll(_authSession.authenticate()); | ||
return headers; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import 'constants.dart'; | ||
|
||
class ApiSettings { | ||
String _baseUrl; | ||
bool _verifySsl; | ||
int _timeout; | ||
String _agentTag; | ||
Function _credentialsCallback; | ||
|
||
ApiSettings.fromMap(Map settings) { | ||
_baseUrl = settings.containsKey('base_url') ? settings['base_url'] : ''; | ||
_verifySsl = | ||
settings.containsKey('verify_ssl') ? settings['verify_ssl'] : true; | ||
_timeout = | ||
settings.containsKey('timeout') ? settings['timeout'] : defaultTimeout; | ||
_agentTag = settings.containsKey('agent_tag') | ||
? settings['agent_tag'] | ||
: '$agentPrefix $lookerVersion'; | ||
_credentialsCallback = settings.containsKey(('credentials_callback')) | ||
? settings['credentials_callback'] | ||
: null; | ||
} | ||
|
||
bool isConfigured() { | ||
return _baseUrl != null; | ||
} | ||
|
||
void readConfig(String section) { | ||
throw UnimplementedError('readConfig'); | ||
} | ||
Comment on lines
+28
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW I think this could be basically the same as the credentialsCallback for consistency with other SDKs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll look look into it. Most of the RTL code comes from my original implementation way back when before I really knew what I was doing (not that I do now :D ). |
||
|
||
String get version { | ||
return apiVersion; | ||
} | ||
|
||
String get baseUrl { | ||
return _baseUrl; | ||
} | ||
|
||
bool get verifySsl { | ||
return _verifySsl; | ||
} | ||
|
||
int get timeout { | ||
return _timeout; | ||
} | ||
|
||
String get agentTag { | ||
return _agentTag; | ||
} | ||
|
||
Function get credentialsCallback { | ||
return _credentialsCallback; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
class DelimList<T> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me rename it for consistency with the other SDKs. Again, this is code that goes way back. |
||
final List<T> _items; | ||
final String _separator; | ||
final String _prefix; | ||
final String _suffix; | ||
|
||
DelimList(List<T> items, | ||
[String separator = ',', String prefix = '', String suffix = '']) | ||
: _items = items, | ||
_separator = separator, | ||
_prefix = prefix, | ||
_suffix = suffix; | ||
|
||
@override | ||
String toString() { | ||
return '$_prefix${_items.join((_separator))}$_suffix'; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to add a Dart install link? Is Dart config supported with Nix? Would be good to find out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me do that.
Not sure I want to get into nix setup for Dart. You cannot use the normal Dart install for cloudtops (you have to download the SDK from an internal google site). Need to figure out how to document "google" specific stuff.