diff --git a/dexcom-library/build.gradle b/dexcom-library/build.gradle new file mode 100644 index 00000000..56f0c6d2 --- /dev/null +++ b/dexcom-library/build.gradle @@ -0,0 +1,49 @@ + +group = 'org.radarbase' +version = '0.0.1' + +apply plugin: 'maven-publish' + +repositories { + // Use jcenter for resolving dependencies. + // You can declare any Maven/Ivy/file repository here. + mavenCentral() +} + +dependencies { + // Use the Kotlin JDK 8 standard library. + implementation libs.kotlin.stdlib + + implementation libs.okhttp + + implementation libs.radar.schemas.commons + + implementation libs.jackson.annotations + + implementation libs.jackson.databind + + implementation libs.avro + + implementation libs.jackson.datatype.jsr310 + + implementation libs.slf4j.api + + // Use the Kotlin test library. + testImplementation libs.kotlin.test + + // Use the Kotlin JUnit integration. + testImplementation libs.kotlin.test.junit +} + +project.afterEvaluate { + publishing { + publications { + library(MavenPublication) { + setGroupId "$group" + setArtifactId "dexcom-library" + version "$version" + from components.java + } + } + } +} \ No newline at end of file diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/DexcomDataConverter.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/DexcomDataConverter.kt new file mode 100644 index 00000000..e92ee722 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/DexcomDataConverter.kt @@ -0,0 +1,37 @@ +package org.radarbase.dexcom.converter + +import com.fasterxml.jackson.databind.JsonNode +import okhttp3.Headers +import org.radarbase.dexcom.request.DexcomRequestGenerator.Companion.JSON_READER +import org.radarbase.dexcom.request.RestRequest +import org.radarbase.dexcom.user.User +import java.time.Instant + +interface DexcomDataConverter : RecordConverter { + fun processRecords( + root: JsonNode, + user: User, + ): Sequence> + + override fun convert( + request: RestRequest, + headers: Headers, + data: ByteArray, + ): List { + val node = JSON_READER.readTree(data) + + return processRecords(node, request.user) + .mapNotNull { result -> + result.fold( + { it }, + { + RecordConverter.logger.error("Data conversion failed: ${it.message}") + null + }, + ) + } + .toList() + } + + fun Instant.toEpoch(): Long = toEpochMilli() / 1000 +} diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/DexcomEGVConverter.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/DexcomEGVConverter.kt new file mode 100644 index 00000000..fe89bf93 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/DexcomEGVConverter.kt @@ -0,0 +1,11 @@ +package org.radarbase.dexcom.converter + +import com.fasterxml.jackson.databind.JsonNode +import org.radarbase.dexcom.user.User + +class DexcomEGVConverter : DexcomDataConverter { + override fun processRecords( + root: JsonNode, + user: User, + ): Sequence> = emptySequence() +} diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/RecordConverter.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/RecordConverter.kt new file mode 100644 index 00000000..b05ac022 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/RecordConverter.kt @@ -0,0 +1,19 @@ +package org.radarbase.dexcom.converter + +import okhttp3.Headers +import org.radarbase.dexcom.request.RestRequest +import org.slf4j.LoggerFactory +import java.io.IOException + +interface RecordConverter { + @Throws(IOException::class) + fun convert( + request: RestRequest, + headers: Headers, + data: ByteArray, + ): List + + companion object { + var logger = LoggerFactory.getLogger(RecordConverter::class.java) + } +} diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/TopicData.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/TopicData.kt new file mode 100644 index 00000000..859e634f --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/converter/TopicData.kt @@ -0,0 +1,11 @@ +package org.radarbase.dexcom.converter + +import org.apache.avro.specific.SpecificRecord + +/** Single value for a topic. */ +data class TopicData( + val topic: String, + val key: SpecificRecord, + val value: SpecificRecord, + val offset: Long, +) \ No newline at end of file diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/offset/Offset.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/offset/Offset.kt new file mode 100644 index 00000000..d2279dd4 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/offset/Offset.kt @@ -0,0 +1,11 @@ +package org.radarbase.dexcom.offset + +import org.radarbase.dexcom.route.Route +import org.radarbase.dexcom.user.User +import java.time.Instant + +data class Offset( + val user: User, + val route: Route, + val offset: Instant, +) \ No newline at end of file diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/offset/Offsets.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/offset/Offsets.kt new file mode 100644 index 00000000..3681411b --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/offset/Offsets.kt @@ -0,0 +1,7 @@ +package org.radarbase.dexcom.offset + +import org.radarbase.dexcom.request.Offset + +data class Offsets( + val offsets: List, +) diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/DexcomOffsetManager.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/DexcomOffsetManager.kt new file mode 100644 index 00000000..842a4b36 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/DexcomOffsetManager.kt @@ -0,0 +1,18 @@ +package org.radarbase.dexcom.request + +import org.radarbase.dexcom.route.Route +import org.radarbase.dexcom.user.User +import java.time.Instant + +data class Offset( + val user: User, + val route: Route, + val offset: Instant, +) + +interface DexcomOffsetManager { + + fun getOffset(route: Route, user: User): Offset? + + fun updateOffsets(route: Route, user: User, offset: Instant) +} diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/DexcomRequestGenerator.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/DexcomRequestGenerator.kt new file mode 100644 index 00000000..3cd00e58 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/DexcomRequestGenerator.kt @@ -0,0 +1,304 @@ +package org.radarbase.dexcom.request + +import com.fasterxml.jackson.core.JsonFactory +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import okhttp3.Response +import okhttp3.ResponseBody +import org.radarbase.dexcom.converter.TopicData +import org.radarbase.dexcom.route.DexcomRouteFactory +import org.radarbase.dexcom.route.Route +import org.radarbase.dexcom.user.User +import org.radarbase.dexcom.user.UserRepository +import org.slf4j.LoggerFactory +import java.io.IOException +import java.time.Duration +import java.time.Instant + +class DexcomRequestGenerator +@JvmOverloads +constructor( + private val userRepository: UserRepository, + private val dexcomOffsetManager: DexcomOffsetManager, + val routes: List = DexcomRouteFactory.getRoutes(userRepository), + private val defaultQueryRange: Duration = Duration.ofDays(15), +) : RequestGenerator { + private val routeNextRequest: MutableMap = mutableMapOf() + + var nextRequestTime: Instant = Instant.MIN + + private val shouldBackoff: Boolean + get() = Instant.now() < nextRequestTime + + override fun requests( + user: User, + max: Int, + ): Sequence { + return routes.asSequence() + .flatMap { route -> + if (routeReady(user, route)) { + generateRequests(route, user) + } else { + logger.info( + "Skip {} for {}: route in backoff until {}", + route, + user.versionedId, + routeNextRequest[routeKey(route, user)], + ) + emptySequence() + } + } + } + + override fun requests( + route: Route, + max: Int, + ): Sequence { + return userRepository + .stream() + .flatMap { user -> + if (routeReady(user, route)) { + generateRequests(route, user) + } else { + logger.info( + "Skip {} for {}: route in backoff until {}", + route, + user.versionedId, + routeNextRequest[routeKey(route, user)], + ) + emptySequence() + } + } + } + + override fun requests( + route: Route, + user: User, + max: Int, + ): Sequence { + return if (routeReady(user, route)) { + generateRequests(route, user) + } else { + logger.info( + "Skip {} for {}: route in backoff until {}", + route, + user.versionedId, + routeNextRequest[routeKey(route, user)], + ) + emptySequence() + } + } + + fun generateRequests( + route: Route, + user: User, + ): Sequence { + val offset = dexcomOffsetManager.getOffset(route, user) + val startDate = user.startDate + val startOffset: Instant = + if (offset == null) { + logger.info("No offsets found for $user, using the start date.") + startDate + } else { + val offsetTime = offset.offset + logger.info("Offsets found in persistence: $offsetTime") + offsetTime.coerceAtLeast(startDate) + } + val endDate = user.endDate?.coerceAtMost(Instant.now()) ?: Instant.now() + if (!startOffset.isBefore(endDate)) { + val userEnd = user.endDate + if (userEnd != null && endDate == userEnd && + Duration.between(userEnd, Instant.now()) > Duration.ofDays(30) + ) { + val key = routeKey(route, user) + routeNextRequest[key] = Instant.MAX + logger.info( + "Disable future requests for {}: user={}, endDate={} (>30d ago), startOffset={}", + route, + user.versionedId, + userEnd, + startOffset, + ) + } + logger.info( + "Skip {} for {}: interval empty (startOffset={} >= endDate={}), " + + "persistedOffset={}, userStartDate={}", + route, + user.versionedId, + startOffset, + endDate, + offset?.offset, + startDate, + ) + return emptySequence() + } + val timeSinceStart = Duration.between(startOffset, Instant.now()) + return if (timeSinceStart > HISTORICAL_DATA_THRESHOLD) { + val endTime = (startOffset + HISTORICAL_QUERY_RANGE).coerceAtMost(endDate) + route.generateRequests(user, startOffset, endTime) + } else { + route.generateRequests(user, startOffset, endDate, USER_MAX_REQUESTS) + } + } + + fun handleResponse( + req: RestRequest, + response: Response, + ): DexcomResult> { + if (response.isSuccessful) { + return DexcomResult.Success(requestSuccessful(req, response)) + } + return try { + DexcomResult.Error(requestFailed(req, response)) + } catch (e: TooManyRequestsException) { + DexcomResult.Success(emptyList()) + } + } + + override fun requestSuccessful( + request: RestRequest, + response: Response, + ): List { + logger.debug("Request successful: {}..", request.request) + val body: ResponseBody = response.body ?: return emptyList() + val data = body.bytes() + val records = + request.route.converters.flatMap { it.convert(request, response.headers, data) } + val offset = records.maxByOrNull { it.offset }?.offset + if (offset != null) { + logger.info("Writing ${records.size} records to offsets...") + val maxOffsetTime = Instant.ofEpochSecond(offset) + val dataAge = Duration.between(maxOffsetTime, Instant.now()) + val nextOffset = if (dataAge <= Duration.ofDays(7)) { + maxOffsetTime.plus(OFFSET_BUFFER) + } else { + maxOf(maxOffsetTime.plus(OFFSET_BUFFER), request.endDate) + } + dexcomOffsetManager.updateOffsets( + request.route, + request.user, + nextOffset, + ) + val nextRequestTime = Instant.now().plus(SUCCESS_BACK_OFF_TIME) + val key = routeKey(request.route, request.user) + routeNextRequest[key] = + routeNextRequest[key]?.let { if (it > nextRequestTime) it else nextRequestTime } + ?: nextRequestTime + } else { + if (request.startDate.plus(TIME_AFTER_REQUEST).isBefore(Instant.now())) { + logger.info("No records found, updating offsets to end date..") + dexcomOffsetManager.updateOffsets( + request.route, + request.user, + request.endDate, + ) + val key = routeKey(request.route, request.user) + routeNextRequest[key] = Instant.now().plus(SUCCESS_BACK_OFF_TIME) + } else { + val key = routeKey(request.route, request.user) + routeNextRequest[key] = Instant.now().plus(BACK_OFF_TIME) + } + } + return records + } + + override fun requestFailed( + request: RestRequest, + response: Response, + ): DexcomError { + return when (response.code) { + 429 -> { + logger.info("Too many requests, rate limit reached. Backing off...") + nextRequestTime = Instant.now().plus(BACK_OFF_TIME) + DexcomRateLimitError("Rate limit reached.", TooManyRequestsException(), "429") + } + 403 -> { + logger.warn( + "User ${request.user} has expired. Please renew the subscription.", + ) + routeNextRequest[routeKey(request.route, request.user)] = + Instant.now().plus(USER_BACK_OFF_TIME) + DexcomAccessForbiddenError( + "Dexcom subscription has expired or API data not available.", + IOException("Forbidden"), + "403", + ) + } + 401 -> { + logger.warn( + "User ${request.user} access token is expired, malformed, or revoked. " + + response.body?.string(), + ) + routeNextRequest[routeKey(request.route, request.user)] = + Instant.now().plus(USER_BACK_OFF_TIME) + DexcomUnauthorizedAccessError( + "Access token expired or revoked.", + IOException("Unauthorized"), + "401", + ) + } + 400 -> { + logger.warn("Client exception.") + nextRequestTime = Instant.now().plus(BACK_OFF_TIME) + routeNextRequest[routeKey(request.route, request.user)] = + Instant.now().plus(BACK_OFF_TIME) + DexcomClientException( + "Client unsupported or unauthorized.", + IOException("Invalid client"), + "400", + ) + } + 422 -> { + logger.warn("Request failed: {}, {}", request, response) + routeNextRequest[routeKey(request.route, request.user)] = + Instant.now().plus(BACK_OFF_TIME) + DexcomValidationError( + response.body?.string().orEmpty(), + IOException("Validation error"), + "422", + ) + } + 404 -> { + logger.warn("Not found.") + routeNextRequest[routeKey(request.route, request.user)] = + Instant.now().plus(BACK_OFF_TIME) + DexcomNotFoundError( + response.body?.string().orEmpty(), + IOException("Data not found"), + "404", + ) + } + else -> { + logger.warn("Request failed: {}, {}", request, response) + routeNextRequest[routeKey(request.route, request.user)] = + Instant.now().plus(BACK_OFF_TIME) + DexcomGenericError( + response.body?.string().orEmpty(), + IOException("Unknown error"), + response.code.toString(), + ) + } + } + } + + private fun routeReady(user: User, route: Route): Boolean { + val key = routeKey(route, user) + return routeNextRequest[key]?.let { Instant.now().isAfter(it) } ?: true + } + + private fun routeKey(route: Route, user: User): String = user.versionedId + "#" + route + + companion object { + private val logger = LoggerFactory.getLogger(DexcomRequestGenerator::class.java) + private val BACK_OFF_TIME = Duration.ofMinutes(10L) + private val TIME_AFTER_REQUEST = Duration.ofDays(30) + private val USER_BACK_OFF_TIME = Duration.ofHours(12L) + private val SUCCESS_BACK_OFF_TIME = Duration.ofSeconds(10L) + private val OFFSET_BUFFER = Duration.ofHours(12) + private val USER_MAX_REQUESTS = 1000 + private val HISTORICAL_DATA_THRESHOLD = Duration.ofDays(365L) + private val HISTORICAL_QUERY_RANGE = Duration.ofDays(365L) + val JSON_FACTORY = JsonFactory() + val JSON_READER = ObjectMapper(JSON_FACTORY).registerModule(JavaTimeModule()).reader() + } +} diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/DexcomResult.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/DexcomResult.kt new file mode 100644 index 00000000..2f153de9 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/DexcomResult.kt @@ -0,0 +1,64 @@ +package org.radarbase.dexcom.request + +sealed class DexcomResult { + data class Success(val value: T) : DexcomResult() + data class Error(val error: DexcomError) : DexcomResult() +} + +sealed interface DexcomError + +sealed class DexcomErrorBase( + val message: String, + val cause: Exception? = null, + val code: String, +) : DexcomError + +class DexcomRateLimitError(message: String, cause: Exception? = null, code: String) : DexcomErrorBase( + message, + cause, + code, +) + +class DexcomClientException(message: String, cause: Exception? = null, code: String) : DexcomErrorBase( + message, + cause, + code, +) + +class DexcomUnauthorizedAccessError( + message: String, + cause: Exception? = null, + code: String, +) : DexcomErrorBase( + message, + cause, + code, +) + +class DexcomAccessForbiddenError( + message: String, + cause: Exception? = null, + code: String, +) : DexcomErrorBase( + message, + cause, + code, +) + +class DexcomValidationError(message: String, cause: Exception? = null, code: String) : DexcomErrorBase( + message, + cause, + code, +) + +class DexcomGenericError(message: String, cause: Exception? = null, code: String) : DexcomErrorBase( + message, + cause, + code, +) + +class DexcomNotFoundError(message: String, cause: Exception? = null, code: String) : DexcomErrorBase( + message, + cause, + code, +) diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/RequestGenerator.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/RequestGenerator.kt new file mode 100644 index 00000000..2a3f00fa --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/RequestGenerator.kt @@ -0,0 +1,23 @@ +package org.radarbase.dexcom.request + +import okhttp3.Response +import org.radarbase.dexcom.converter.TopicData +import org.radarbase.dexcom.route.Route +import org.radarbase.dexcom.user.User + + +interface RequestGenerator { + + fun requests(user: User, max: Int): Sequence + + fun requests(route: Route, user: User, max: Int): Sequence + + fun requests(route: Route, max: Int): Sequence + + fun requestSuccessful(request: RestRequest, response: Response): List + + fun requestFailed(request: RestRequest, response: Response): DexcomError +} + + + diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/RestRequest.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/RestRequest.kt new file mode 100644 index 00000000..acb988d9 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/RestRequest.kt @@ -0,0 +1,14 @@ +package org.radarbase.dexcom.request + +import okhttp3.Request +import org.radarbase.dexcom.route.DexcomRoute +import org.radarbase.dexcom.user.User +import java.time.Instant + +data class RestRequest( + val request: Request, + val user: User, + val route: DexcomRoute, + val startDate: Instant, + val endDate: Instant, +) diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/TooManyRequestsException.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/TooManyRequestsException.kt new file mode 100644 index 00000000..fe37406f --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/request/TooManyRequestsException.kt @@ -0,0 +1,3 @@ +package org.radarbase.dexcom.request + +class TooManyRequestsException : RuntimeException() diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/DexcomEGVRoute.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/DexcomEGVRoute.kt new file mode 100644 index 00000000..b3844c6a --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/DexcomEGVRoute.kt @@ -0,0 +1,14 @@ +package org.radarbase.dexcom.route + +import org.radarbase.dexcom.converter.DexcomDataConverter +import org.radarbase.dexcom.user.UserRepository + +class DexcomEGVRoute( + userRepository: UserRepository, +) : DexcomRoute(userRepository) { + override fun subPath(): String = "egvs" + + override fun toString(): String = "dexcom_egv" + + override val converters: List = listOf(DexcomEGVConverter()) +} diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/DexcomRoute.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/DexcomRoute.kt new file mode 100644 index 00000000..263a48a8 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/DexcomRoute.kt @@ -0,0 +1,72 @@ +package org.radarbase.dexcom.route + +import okhttp3.Request +import org.radarbase.dexcom.converter.DexcomDataConverter +import org.radarbase.dexcom.request.RestRequest +import org.radarbase.dexcom.user.User +import org.radarbase.dexcom.user.UserRepository +import java.time.Duration +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +abstract class DexcomRoute( + private val userRepository: UserRepository, + override val maxIntervalPerRequest: Duration = DEFAULT_INTERVAL_PER_REQUEST, +) : Route { + abstract val converters: List + + fun createRequest(user: User, baseUrl: String, queryParams: String): Request { + val accessToken = userRepository.getAccessToken(user) + return Request.Builder() + .url(baseUrl + queryParams) + .header("Authorization", "Bearer $accessToken") + .get() + .build() + } + + override fun generateRequests( + user: User, + start: Instant, + end: Instant, + ): Sequence { + val request = createRequest( + user, + "$DEXCOM_API_BASE_URL/${subPath()}", + "?startDate=${start.toDexcomDate()}&endDate=${end.toDexcomDate()}", + ) + return sequenceOf(RestRequest(request, user, this, start, end)) + } + + override fun generateRequests( + user: User, + start: Instant, + end: Instant, + max: Int, + ): Sequence { + return generateSequence(start) { it + maxIntervalPerRequest } + .takeWhile { it < end } + .take(max) + .map { startRange -> + val endRange = (startRange + maxIntervalPerRequest).coerceAtMost(end) + val request = createRequest( + user, + "$DEXCOM_API_BASE_URL/${subPath()}", + "?startDate=${startRange.toDexcomDate()}&endDate=${endRange.toDexcomDate()}", + ) + RestRequest(request, user, this, startRange, endRange) + } + } + + abstract fun subPath(): String + + fun Instant.toDexcomDate(): String = + LocalDateTime.ofInstant(this, ZoneOffset.UTC).format(DEXCOM_DATE_FORMAT) + + companion object { + const val DEXCOM_API_BASE_URL = "https://api.dexcom.com/v3/users/self" + private val DEXCOM_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") + private val DEFAULT_INTERVAL_PER_REQUEST = Duration.ofDays(30L) + } +} diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/DexcomRouteFactory.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/DexcomRouteFactory.kt new file mode 100644 index 00000000..ee8daa73 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/DexcomRouteFactory.kt @@ -0,0 +1,12 @@ +package org.radarbase.dexcom.route + +import org.radarbase.dexcom.user.UserRepository + +object DexcomRouteFactory { + + fun getRoutes(userRepository: UserRepository): List { + return listOf( + DexcomEGVRoute(userRepository), + ) + } +} \ No newline at end of file diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/Route.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/Route.kt new file mode 100644 index 00000000..e33d3a6a --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/route/Route.kt @@ -0,0 +1,23 @@ +package org.radarbase.dexcom.route + +import org.radarbase.dexcom.request.RestRequest +import org.radarbase.dexcom.user.User +import java.time.Duration +import java.time.Instant + +interface Route { + + fun generateRequests(user: User, start: Instant, end: Instant): Sequence + + fun generateRequests(user: User, start: Instant, end: Instant, max: Int): Sequence + + /** + * This is how it would appear in the offsets + */ + override fun toString(): String + + /** + * The number of days to request in a single request of this route. + */ + val maxIntervalPerRequest: Duration +} \ No newline at end of file diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/user/User.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/user/User.kt new file mode 100644 index 00000000..da7414b6 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/user/User.kt @@ -0,0 +1,22 @@ +package org.radarbase.dexcom.user + +import org.radarcns.kafka.ObservationKey +import java.time.Instant + +interface User { + val id: String + val projectId: String + val userId: String + val sourceId: String + val externalId: String? + val startDate: Instant + val endDate: Instant? + val createdAt: Instant + val humanReadableUserId: String? + val serviceUserId: String? + val version: String? + val isAuthorized: Boolean + + val observationKey: ObservationKey + val versionedId: String +} diff --git a/dexcom-library/src/main/kotlin/org/radarbase/dexcom/user/UserRepository.kt b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/user/UserRepository.kt new file mode 100644 index 00000000..438acf85 --- /dev/null +++ b/dexcom-library/src/main/kotlin/org/radarbase/dexcom/user/UserRepository.kt @@ -0,0 +1,14 @@ +package org.radarbase.dexcom.user + +import java.io.IOException + +interface UserRepository { + @Throws(IOException::class) + operator fun get(key: String): User? + + @Throws(IOException::class) + fun stream(): Sequence + + @Throws(IOException::class) + fun getAccessToken(user: User): String +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 64f23944..2a682abe 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,8 @@ include(":kafka-connect-fitbit-source") include(":kafka-connect-rest-source") include(":kafka-connect-oura-source") include(":oura-library") +include(":dexcom-library") +include(":kafka-connect-dexcom-source") pluginManagement { repositories {