-
Notifications
You must be signed in to change notification settings - Fork 199
feat: Looker SDK generator for the Dart language #933
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
base: main
Are you sure you want to change the base?
Changes from 3 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 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 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). | ||||||
bryans99 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
``` | ||||||
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. | ||||||
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. 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 commentThe 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. |
||||||
|
||||||
### Generate | ||||||
|
||||||
Run `yarn sdk Gen` from the `{reporoot}`. Note that the SDK generator needs to be built `yarn build`. If changing the generator run 'yarn watch` in a separate window. This command generates two files: | ||||||
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. should this be
Suggested change
|
||||||
|
||||||
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 this test requires 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 linter and will fail if the files have not been correctly formatted. | ||||||
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. This can be hooked into the reformatter for codegen |
||||||
|
||||||
### Run format-check | ||||||
|
||||||
Run `yarn format-check` from `{reporoot}/dart/looker_sdk` to check the formatting of the `dart` files. | ||||||
|
||||||
### Run analyze | ||||||
|
||||||
Run `yarn format-analyze` from `{reporoot}/dart/looker_sdk` to lint the `dart` files. | ||||||
|
||||||
## 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 diaplayed 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?). | ||||||
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.
Suggested change
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. enums get generated if they're in the API specification file |
||||||
2. Add optional timeout option to methods. | ||||||
3. Add additional authorization methods to api keys. | ||||||
4. Revisit auth session. There is some duplication of methods 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. | ||||||
bryans99 marked this conversation as resolved.
Show resolved
Hide resolved
|
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'; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.