Skip to content

Commit ffd2280

Browse files
authored
Relax platform requirements to support macOS 13 (#7)
* Relax platform requirements to support macOS 13 * Conditionalize HuggingFaceAuthenticationManagerTests to avoid availability issue
1 parent e9e663c commit ffd2280

File tree

4 files changed

+146
-103
lines changed

4 files changed

+146
-103
lines changed

Package.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import PackageDescription
66
let package = Package(
77
name: "swift-huggingface",
88
platforms: [
9-
.macOS(.v14),
10-
.macCatalyst(.v14),
9+
.macOS(.v13),
10+
.macCatalyst(.v16),
1111
.iOS(.v16),
12-
.watchOS(.v10),
13-
.tvOS(.v17),
12+
.watchOS(.v9),
13+
.tvOS(.v16),
1414
.visionOS(.v1),
1515
],
1616
products: [

Sources/HuggingFace/OAuth/HuggingFaceAuthenticationManager.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
/// A manager for handling Hugging Face OAuth authentication.
66
///
77
/// - SeeAlso: [Hugging Face OAuth Documentation](https://huggingface.co/docs/api-inference/authentication)
8+
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
89
@Observable
910
@MainActor
1011
public final class HuggingFaceAuthenticationManager: Sendable {
@@ -184,6 +185,7 @@
184185

185186
// MARK: -
186187

188+
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
187189
extension HuggingFaceAuthenticationManager {
188190
/// OAuth scopes supported by HuggingFace
189191
public enum Scope: Hashable, Sendable {
@@ -247,6 +249,7 @@
247249
}
248250
}
249251

252+
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
250253
extension HuggingFaceAuthenticationManager.Scope: RawRepresentable {
251254
public init(rawValue: String) {
252255
switch rawValue {
@@ -299,6 +302,7 @@
299302
}
300303
}
301304

305+
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
302306
extension HuggingFaceAuthenticationManager.Scope: Codable {
303307
public init(from decoder: Decoder) throws {
304308
let container = try decoder.singleValueContainer()
@@ -312,12 +316,14 @@
312316
}
313317
}
314318

319+
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
315320
extension HuggingFaceAuthenticationManager.Scope: ExpressibleByStringLiteral {
316321
public init(stringLiteral value: String) {
317322
self = Self(rawValue: value)
318323
}
319324
}
320325

326+
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
321327
extension Set<HuggingFaceAuthenticationManager.Scope> {
322328
public static var basic: Self { [.openid, .profile, .email] }
323329
public static var readAccess: Self { [.openid, .profile, .email, .readRepos] }
@@ -329,6 +335,7 @@
329335

330336
// MARK: -
331337

338+
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
332339
extension HuggingFaceAuthenticationManager {
333340
/// A mechanism for storing and retrieving OAuth tokens.
334341
public struct TokenStorage: Sendable {

Sources/HuggingFace/Shared/TokenProvider.swift

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@ import Foundation
3434
///
3535
/// ## OAuth Authentication
3636
///
37-
/// For OAuth-based authentication, use the `.oauth` case with an authentication manager:
37+
/// For OAuth-based authentication (requires macOS 14+, iOS 17+), use the `.oauth(manager:)` factory method:
3838
///
3939
/// ```swift
40-
/// let authManager = HuggingFaceAuthenticationManager(
40+
/// let authManager = try HuggingFaceAuthenticationManager(
4141
/// clientID: "your-client-id",
42-
/// redirectURL: URL(string: "myapp://oauth")!
42+
/// redirectURL: URL(string: "myapp://oauth")!,
43+
/// scope: .basic,
44+
/// keychainService: "com.example.app",
45+
/// keychainAccount: "huggingface"
4346
/// )
4447
/// let client = HubClient(tokenProvider: .oauth(manager: authManager))
4548
/// ```
@@ -117,13 +120,13 @@ public indirect enum TokenProvider: Sendable {
117120
/// the same token detection logic as the Hugging Face CLI.
118121
case environment
119122

120-
/// An OAuth token provider that uses HuggingFaceAuthenticationManager.
123+
/// An OAuth token provider that retrieves tokens asynchronously.
121124
///
122-
/// Use this case for OAuth-based authentication flows. The authentication
123-
/// manager handles the complete OAuth flow including token refresh.
125+
/// Use this case for OAuth-based authentication flows. Create instances using
126+
/// the `TokenProvider.oauth(manager:)` factory method when using `HuggingFaceAuthenticationManager`.
124127
///
125-
/// - Parameter manager: The OAuth authentication manager that handles token retrieval and refresh.
126-
case oauth(manager: HuggingFaceAuthenticationManager)
128+
/// - Parameter getToken: A closure that retrieves a valid OAuth token.
129+
case oauth(getToken: @Sendable () async throws -> String)
127130

128131
/// A composite token provider that tries multiple providers in order.
129132
///
@@ -185,7 +188,7 @@ public indirect enum TokenProvider: Sendable {
185188
case .environment:
186189
return try getTokenFromEnvironment()
187190

188-
case .oauth(let manager):
191+
case .oauth:
189192
fatalError(
190193
"OAuth token provider requires async context. Use getToken() in an async context or switch to a synchronous provider."
191194
)
@@ -209,6 +212,39 @@ public indirect enum TokenProvider: Sendable {
209212
}
210213
}
211214

215+
// MARK: - OAuth Factory
216+
217+
#if canImport(AuthenticationServices)
218+
import Observation
219+
220+
extension TokenProvider {
221+
/// Creates an OAuth token provider using HuggingFaceAuthenticationManager.
222+
///
223+
/// Use this factory method for OAuth-based authentication flows. The authentication
224+
/// manager handles the complete OAuth flow including token refresh.
225+
///
226+
/// ```swift
227+
/// let authManager = try HuggingFaceAuthenticationManager(
228+
/// clientID: "your-client-id",
229+
/// redirectURL: URL(string: "myapp://oauth")!,
230+
/// scope: .basic,
231+
/// keychainService: "com.example.app",
232+
/// keychainAccount: "huggingface"
233+
/// )
234+
/// let client = HubClient(tokenProvider: .oauth(manager: authManager))
235+
/// ```
236+
///
237+
/// - Parameter manager: The OAuth authentication manager that handles token retrieval and refresh.
238+
/// - Returns: A token provider that retrieves tokens from the authentication manager.
239+
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
240+
public static func oauth(manager: HuggingFaceAuthenticationManager) -> TokenProvider {
241+
return .oauth(getToken: { @MainActor in
242+
try await manager.getValidToken()
243+
})
244+
}
245+
}
246+
#endif
247+
212248
// MARK: - ExpressibleByStringLiteral & ExpressibleByStringInterpolation
213249

214250
extension TokenProvider: ExpressibleByStringLiteral, ExpressibleByStringInterpolation {

Tests/HuggingFaceTests/OAuthTests/HuggingFaceAuthenticationManagerTests.swift

Lines changed: 90 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -89,100 +89,100 @@ import Testing
8989
}
9090
}
9191
}
92-
#endif // swift(>=6.1)
9392

94-
@Suite("Hugging Face OAuth Scope Tests", .serialized)
95-
struct HuggingFaceScopeTests {
96-
typealias Scope = HuggingFaceAuthenticationManager.Scope
97-
98-
@Test("OAuth Scope sets work correctly")
99-
func testScopeSets() {
100-
// Test basic scope set
101-
let basicScopes = Set<Scope>.basic
102-
#expect(basicScopes.contains(.openid))
103-
#expect(basicScopes.contains(.profile))
104-
#expect(basicScopes.contains(.email))
105-
106-
// Test read access scope set
107-
let readScopes = Set<Scope>.readAccess
108-
#expect(readScopes.contains(.readRepos))
109-
110-
// Test write access scope set
111-
let writeScopes = Set<Scope>.writeAccess
112-
#expect(writeScopes.contains(.writeRepos))
113-
114-
// Test full access scope set
115-
let fullScopes = Set<Scope>.fullAccess
116-
#expect(fullScopes.contains(.manageRepos))
117-
#expect(fullScopes.contains(.inferenceAPI))
118-
119-
// Test inference only scope set
120-
let inferenceScopes = Set<Scope>.inferenceOnly
121-
#expect(inferenceScopes.contains(.openid))
122-
#expect(inferenceScopes.contains(.inferenceAPI))
123-
124-
// Test discussions scope set
125-
let discussionScopes = Set<Scope>.discussions
126-
#expect(discussionScopes.contains(.writeDiscussions))
127-
}
93+
@Suite("Hugging Face OAuth Scope Tests", .serialized)
94+
struct HuggingFaceScopeTests {
95+
typealias Scope = HuggingFaceAuthenticationManager.Scope
96+
97+
@Test("OAuth Scope sets work correctly")
98+
func testScopeSets() {
99+
// Test basic scope set
100+
let basicScopes = Set<Scope>.basic
101+
#expect(basicScopes.contains(.openid))
102+
#expect(basicScopes.contains(.profile))
103+
#expect(basicScopes.contains(.email))
104+
105+
// Test read access scope set
106+
let readScopes = Set<Scope>.readAccess
107+
#expect(readScopes.contains(.readRepos))
108+
109+
// Test write access scope set
110+
let writeScopes = Set<Scope>.writeAccess
111+
#expect(writeScopes.contains(.writeRepos))
112+
113+
// Test full access scope set
114+
let fullScopes = Set<Scope>.fullAccess
115+
#expect(fullScopes.contains(.manageRepos))
116+
#expect(fullScopes.contains(.inferenceAPI))
117+
118+
// Test inference only scope set
119+
let inferenceScopes = Set<Scope>.inferenceOnly
120+
#expect(inferenceScopes.contains(.openid))
121+
#expect(inferenceScopes.contains(.inferenceAPI))
122+
123+
// Test discussions scope set
124+
let discussionScopes = Set<Scope>.discussions
125+
#expect(discussionScopes.contains(.writeDiscussions))
126+
}
128127

129-
@Test("OAuth Scope raw values are correct")
130-
func testScopeRawValues() {
131-
#expect(Scope.openid.rawValue == "openid")
132-
#expect(Scope.profile.rawValue == "profile")
133-
#expect(Scope.email.rawValue == "email")
134-
#expect(Scope.readBilling.rawValue == "read-billing")
135-
#expect(Scope.readRepos.rawValue == "read-repos")
136-
#expect(Scope.writeRepos.rawValue == "write-repos")
137-
#expect(Scope.manageRepos.rawValue == "manage-repos")
138-
#expect(Scope.inferenceAPI.rawValue == "inference-api")
139-
#expect(Scope.writeDiscussions.rawValue == "write-discussions")
140-
141-
// Test custom scope
142-
let customScope = Scope.other("custom-scope")
143-
#expect(customScope.rawValue == "custom-scope")
144-
}
128+
@Test("OAuth Scope raw values are correct")
129+
func testScopeRawValues() {
130+
#expect(Scope.openid.rawValue == "openid")
131+
#expect(Scope.profile.rawValue == "profile")
132+
#expect(Scope.email.rawValue == "email")
133+
#expect(Scope.readBilling.rawValue == "read-billing")
134+
#expect(Scope.readRepos.rawValue == "read-repos")
135+
#expect(Scope.writeRepos.rawValue == "write-repos")
136+
#expect(Scope.manageRepos.rawValue == "manage-repos")
137+
#expect(Scope.inferenceAPI.rawValue == "inference-api")
138+
#expect(Scope.writeDiscussions.rawValue == "write-discussions")
139+
140+
// Test custom scope
141+
let customScope = Scope.other("custom-scope")
142+
#expect(customScope.rawValue == "custom-scope")
143+
}
145144

146-
@Test("OAuth Scope initialization from raw values")
147-
func testScopeInitializationFromRawValue() {
148-
#expect(Scope(rawValue: "openid") == .openid)
149-
#expect(Scope(rawValue: "profile") == .profile)
150-
#expect(Scope(rawValue: "email") == .email)
151-
#expect(Scope(rawValue: "read-billing") == .readBilling)
152-
#expect(Scope(rawValue: "read-repos") == .readRepos)
153-
#expect(Scope(rawValue: "write-repos") == .writeRepos)
154-
#expect(Scope(rawValue: "manage-repos") == .manageRepos)
155-
#expect(Scope(rawValue: "inference-api") == .inferenceAPI)
156-
#expect(Scope(rawValue: "write-discussions") == .writeDiscussions)
157-
158-
// Test custom scope
159-
let customScope = Scope(rawValue: "custom-scope")
160-
#expect(customScope == .other("custom-scope"))
161-
}
145+
@Test("OAuth Scope initialization from raw values")
146+
func testScopeInitializationFromRawValue() {
147+
#expect(Scope(rawValue: "openid") == .openid)
148+
#expect(Scope(rawValue: "profile") == .profile)
149+
#expect(Scope(rawValue: "email") == .email)
150+
#expect(Scope(rawValue: "read-billing") == .readBilling)
151+
#expect(Scope(rawValue: "read-repos") == .readRepos)
152+
#expect(Scope(rawValue: "write-repos") == .writeRepos)
153+
#expect(Scope(rawValue: "manage-repos") == .manageRepos)
154+
#expect(Scope(rawValue: "inference-api") == .inferenceAPI)
155+
#expect(Scope(rawValue: "write-discussions") == .writeDiscussions)
156+
157+
// Test custom scope
158+
let customScope = Scope(rawValue: "custom-scope")
159+
#expect(customScope == .other("custom-scope"))
160+
}
162161

163-
@Test("OAuth Scope descriptions are correct")
164-
func testScopeDescriptions() {
165-
#expect(Scope.openid.description.contains("ID token"))
166-
#expect(Scope.profile.description.contains("profile information"))
167-
#expect(Scope.email.description.contains("email address"))
168-
#expect(Scope.readBilling.description.contains("payment method"))
169-
#expect(Scope.readRepos.description.contains("read access"))
170-
#expect(Scope.writeRepos.description.contains("write/read access"))
171-
#expect(Scope.manageRepos.description.contains("full access"))
172-
#expect(Scope.inferenceAPI.description.contains("Inference API"))
173-
#expect(Scope.writeDiscussions.description.contains("discussions"))
174-
175-
// Test custom scope description
176-
let customScope = Scope.other("custom-scope")
177-
#expect(customScope.description == "custom-scope")
178-
}
162+
@Test("OAuth Scope descriptions are correct")
163+
func testScopeDescriptions() {
164+
#expect(Scope.openid.description.contains("ID token"))
165+
#expect(Scope.profile.description.contains("profile information"))
166+
#expect(Scope.email.description.contains("email address"))
167+
#expect(Scope.readBilling.description.contains("payment method"))
168+
#expect(Scope.readRepos.description.contains("read access"))
169+
#expect(Scope.writeRepos.description.contains("write/read access"))
170+
#expect(Scope.manageRepos.description.contains("full access"))
171+
#expect(Scope.inferenceAPI.description.contains("Inference API"))
172+
#expect(Scope.writeDiscussions.description.contains("discussions"))
173+
174+
// Test custom scope description
175+
let customScope = Scope.other("custom-scope")
176+
#expect(customScope.description == "custom-scope")
177+
}
179178

180-
@Test("OAuth Scope string literal support")
181-
func testScopeStringLiteral() {
182-
let scope: Scope = "openid"
183-
#expect(scope == .openid)
179+
@Test("OAuth Scope string literal support")
180+
func testScopeStringLiteral() {
181+
let scope: Scope = "openid"
182+
#expect(scope == .openid)
184183

185-
let customScope: Scope = "custom-scope"
186-
#expect(customScope == .other("custom-scope"))
184+
let customScope: Scope = "custom-scope"
185+
#expect(customScope == .other("custom-scope"))
186+
}
187187
}
188-
}
188+
#endif // swift(>=6.1)

0 commit comments

Comments
 (0)