Skip to content
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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions dart/looker_sdk/.gitignore
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

78 changes: 78 additions & 0 deletions dart/looker_sdk/README.md
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.
bryans99 marked this conversation as resolved.
Show resolved Hide resolved

## 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.
Copy link
Contributor

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.

Copy link
Collaborator Author

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.


### 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.
bryans99 marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions dart/looker_sdk/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include: package:lints/recommended.yaml

analyzer:
108 changes: 108 additions & 0 deletions dart/looker_sdk/example/main.dart
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW if you want to have a test for binary payloads, content_thumbnail is pretty quick for PNG

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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']};
}
1 change: 1 addition & 0 deletions dart/looker_sdk/lib/index.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'src/looker_sdk.dart';
10 changes: 10 additions & 0 deletions dart/looker_sdk/lib/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';
100 changes: 100 additions & 0 deletions dart/looker_sdk/lib/src/rtl/api_methods.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;
}
}
55 changes: 55 additions & 0 deletions dart/looker_sdk/lib/src/rtl/api_settings.dart
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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;
}
}
18 changes: 18 additions & 0 deletions dart/looker_sdk/lib/src/rtl/api_types.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class DelimList<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that DelimList is shorter than DelimArray. Renaming in other SDKs is probably not worth the breakage.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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';
}
}
Loading