Initial commit
This commit is contained in:
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"fantomas": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"commands": [
|
||||||
|
"fantomas"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
.editorconfig
Normal file
41
.editorconfig
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
root=true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset=utf-8
|
||||||
|
end_of_line=crlf
|
||||||
|
trim_trailing_whitespace=true
|
||||||
|
insert_final_newline=true
|
||||||
|
indent_style=space
|
||||||
|
indent_size=4
|
||||||
|
|
||||||
|
# ReSharper properties
|
||||||
|
resharper_xml_indent_size=2
|
||||||
|
resharper_xml_max_line_length=100
|
||||||
|
resharper_xml_tab_width=2
|
||||||
|
|
||||||
|
[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[*.{fs,fsi}]
|
||||||
|
fsharp_bar_before_discriminated_union_declaration=true
|
||||||
|
fsharp_space_before_uppercase_invocation=true
|
||||||
|
fsharp_space_before_class_constructor=true
|
||||||
|
fsharp_space_before_member=true
|
||||||
|
fsharp_space_before_colon=true
|
||||||
|
fsharp_space_before_semicolon=true
|
||||||
|
fsharp_multiline_bracket_style=aligned
|
||||||
|
fsharp_newline_between_type_definition_and_members=true
|
||||||
|
fsharp_align_function_signature_to_indentation=true
|
||||||
|
fsharp_alternative_long_member_definitions=true
|
||||||
|
fsharp_multi_line_lambda_closing_newline=true
|
||||||
|
fsharp_experimental_keep_indent_in_branch=true
|
||||||
|
fsharp_max_value_binding_width=80
|
||||||
|
fsharp_max_record_width=0
|
||||||
|
max_line_length=120
|
||||||
|
end_of_line=lf
|
||||||
|
|
||||||
|
[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
tab_width=2
|
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
* eol=auto
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.nix text eol=lf
|
||||||
|
hooks/pre-push text eol=lf
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
|
.idea/
|
||||||
|
*.user
|
||||||
|
*.DotSettings
|
||||||
|
.DS_Store
|
||||||
|
result
|
||||||
|
.profile*
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
10
.woodpecker/.all-checks-complete.yml
Normal file
10
.woodpecker/.all-checks-complete.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
steps:
|
||||||
|
echo:
|
||||||
|
image: alpine
|
||||||
|
commands:
|
||||||
|
- echo "All required checks complete"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
||||||
|
skip_clone: true
|
22
.woodpecker/.build.yml
Normal file
22
.woodpecker/.build.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
steps:
|
||||||
|
build:
|
||||||
|
image: nixos/nix
|
||||||
|
commands:
|
||||||
|
- echo 'experimental-features = flakes nix-command' >> /etc/nix/nix.conf
|
||||||
|
# Lint
|
||||||
|
- "nix flake check"
|
||||||
|
# Test
|
||||||
|
- nix build
|
||||||
|
- nix run . -- --help
|
||||||
|
- nix run . -- auth --help
|
||||||
|
- nix run . -- lookup-gym --help
|
||||||
|
- nix run . -- fullness --help
|
||||||
|
- nix run . -- activity --help
|
||||||
|
- nix develop --command markdown-link-check README.md
|
||||||
|
- nix develop --command dotnet test
|
||||||
|
- nix develop --command dotnet test --configuration Release
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: "push"
|
||||||
|
evaluate: 'CI_COMMIT_BRANCH == CI_REPO_DEFAULT_BRANCH'
|
||||||
|
- event: "pull_request"
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Patrick Stevens
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
21
PureGym.App/ArgsCrate.fs
Normal file
21
PureGym.App/ArgsCrate.fs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace PureGym.App
|
||||||
|
|
||||||
|
open System.Threading.Tasks
|
||||||
|
open Argu
|
||||||
|
|
||||||
|
type ArgsEvaluator<'ret> =
|
||||||
|
abstract Eval<'a, 'b when 'b :> IArgParserTemplate> :
|
||||||
|
(ParseResults<'b> -> Result<'a, ArguParseException>) -> ('a -> Task<int>) -> 'ret
|
||||||
|
|
||||||
|
type ArgsCrate =
|
||||||
|
abstract Apply<'ret> : ArgsEvaluator<'ret> -> 'ret
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module ArgsCrate =
|
||||||
|
let make<'a, 'b when 'b :> IArgParserTemplate>
|
||||||
|
(ofResult : ParseResults<'b> -> Result<'a, ArguParseException>)
|
||||||
|
(run : 'a -> Task<int>)
|
||||||
|
=
|
||||||
|
{ new ArgsCrate with
|
||||||
|
member _.Apply e = e.Eval ofResult run
|
||||||
|
}
|
41
PureGym.App/AuthArg.fs
Normal file
41
PureGym.App/AuthArg.fs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
namespace PureGym.App
|
||||||
|
|
||||||
|
open Argu
|
||||||
|
open PureGym
|
||||||
|
|
||||||
|
type AuthArg =
|
||||||
|
| [<Unique ; CustomAppSettings "PUREGYM_BEARER_TOKEN">] Bearer_Token of string
|
||||||
|
| [<Unique>] User_Email of string
|
||||||
|
| [<Unique>] Pin of string
|
||||||
|
| [<GatherUnrecognized>] Others of string
|
||||||
|
|
||||||
|
interface IArgParserTemplate with
|
||||||
|
member s.Usage =
|
||||||
|
match s with
|
||||||
|
| AuthArg.Bearer_Token _ -> "A bearer token for the PureGym API"
|
||||||
|
| AuthArg.User_Email _ -> "PureGym user's email address"
|
||||||
|
| AuthArg.Pin _ -> "Eight-digit PureGym user's PIN"
|
||||||
|
| AuthArg.Others _ -> "<specific args for command>"
|
||||||
|
|
||||||
|
static member Parse (args : ParseResults<AuthArg>) : Result<Auth * string[], ArguParseException> =
|
||||||
|
let unmatchedArgs = args.GetResults AuthArg.Others |> List.toArray
|
||||||
|
|
||||||
|
match
|
||||||
|
args.TryGetResult AuthArg.User_Email, args.TryGetResult AuthArg.Pin, args.TryGetResult AuthArg.Bearer_Token
|
||||||
|
with
|
||||||
|
| Some email, Some pin, _ ->
|
||||||
|
let auth =
|
||||||
|
Auth.User
|
||||||
|
{
|
||||||
|
Username = email
|
||||||
|
Pin = pin
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok (auth, unmatchedArgs)
|
||||||
|
| Some _email, None, _ -> failwith "Supplied --user-email but no --pin; either both or neither are required."
|
||||||
|
| None, Some _pin, _ -> failwith "Supplied --pin but no --user-email; either both or neither are required."
|
||||||
|
| None, None, None ->
|
||||||
|
failwith "No creds given: expected at least one of `--bearer-token` or `--user-email --pin`"
|
||||||
|
| None, None, Some token ->
|
||||||
|
let auth = Auth.Token (AuthToken.ofBearerToken token)
|
||||||
|
Ok (auth, unmatchedArgs)
|
40
PureGym.App/Authenticate.fs
Normal file
40
PureGym.App/Authenticate.fs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
namespace PureGym.App
|
||||||
|
|
||||||
|
open Argu
|
||||||
|
open System
|
||||||
|
open PureGym
|
||||||
|
|
||||||
|
type GetTokenArg =
|
||||||
|
| [<ExactlyOnce>] User_Email of string
|
||||||
|
| [<ExactlyOnce>] Pin of string
|
||||||
|
|
||||||
|
interface IArgParserTemplate with
|
||||||
|
member s.Usage =
|
||||||
|
match s with
|
||||||
|
| GetTokenArg.Pin _ -> "Eight-digit PureGym user's PIN"
|
||||||
|
| GetTokenArg.User_Email _ -> "PureGym user's email address"
|
||||||
|
|
||||||
|
static member Parse (args : ParseResults<GetTokenArg>) : Result<UsernamePin, ArguParseException> =
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Username = args.GetResult GetTokenArg.User_Email
|
||||||
|
Pin = args.GetResult GetTokenArg.Pin
|
||||||
|
}
|
||||||
|
|> Ok
|
||||||
|
with :? ArguParseException as e ->
|
||||||
|
Error e
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Authenticate =
|
||||||
|
|
||||||
|
let run (creds : UsernamePin) =
|
||||||
|
task {
|
||||||
|
let! cred = AuthToken.get creds
|
||||||
|
Console.WriteLine cred.AccessToken
|
||||||
|
|
||||||
|
match cred.ExpiryTime with
|
||||||
|
| None -> ()
|
||||||
|
| Some expiry -> Console.Error.WriteLine $"Expires at {expiry}"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
11
PureGym.App/Exception.fs
Normal file
11
PureGym.App/Exception.fs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace PureGym.App
|
||||||
|
|
||||||
|
open System.Runtime.ExceptionServices
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Exception =
|
||||||
|
|
||||||
|
let reraiseWithOriginalStackTrace<'a> (e : exn) : 'a =
|
||||||
|
let edi = ExceptionDispatchInfo.Capture e
|
||||||
|
edi.Throw ()
|
||||||
|
failwith "unreachable"
|
61
PureGym.App/Fullness.fs
Normal file
61
PureGym.App/Fullness.fs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
namespace PureGym.App
|
||||||
|
|
||||||
|
open Argu
|
||||||
|
open PureGym
|
||||||
|
|
||||||
|
type FullnessArgsFragment =
|
||||||
|
| [<Unique>] Gym_Id of int
|
||||||
|
| [<Unique>] Gym_Name of string
|
||||||
|
| [<Unique>] Terse of bool
|
||||||
|
|
||||||
|
interface IArgParserTemplate with
|
||||||
|
member s.Usage =
|
||||||
|
match s with
|
||||||
|
| FullnessArgsFragment.Gym_Id _ -> "ID of the gym to look up, according to PureGym's internal mapping"
|
||||||
|
| FullnessArgsFragment.Gym_Name _ -> "Name of a gym (best-guess fuzzy matching)"
|
||||||
|
| FullnessArgsFragment.Terse _ -> "If true, output only the single number 'how many people are in the gym'."
|
||||||
|
|
||||||
|
type FullnessArgs =
|
||||||
|
{
|
||||||
|
Creds : Auth
|
||||||
|
Gym : GymSelector
|
||||||
|
Terse : bool
|
||||||
|
}
|
||||||
|
|
||||||
|
static member Parse
|
||||||
|
(auth : Auth)
|
||||||
|
(args : FullnessArgsFragment ParseResults)
|
||||||
|
: Result<FullnessArgs, ArguParseException>
|
||||||
|
=
|
||||||
|
let gym =
|
||||||
|
match args.TryGetResult FullnessArgsFragment.Gym_Id, args.TryGetResult FullnessArgsFragment.Gym_Name with
|
||||||
|
| Some _, Some _ -> failwith "--gym-id and --gym-name are mutually exclusive"
|
||||||
|
| None, None ->
|
||||||
|
System.Console.Error.WriteLine "No gym ID given and no gym named; assuming the user's home gym"
|
||||||
|
GymSelector.Home
|
||||||
|
| Some id, None -> GymSelector.Id id
|
||||||
|
| None, Some name -> GymSelector.Name name
|
||||||
|
|
||||||
|
{
|
||||||
|
Creds = auth
|
||||||
|
Gym = gym
|
||||||
|
Terse = args.TryGetResult FullnessArgsFragment.Terse |> Option.defaultValue false
|
||||||
|
}
|
||||||
|
|> Ok
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Fullness =
|
||||||
|
|
||||||
|
let run (args : FullnessArgs) =
|
||||||
|
task {
|
||||||
|
let! client = Api.make args.Creds
|
||||||
|
let! id = GymSelector.canonicalId client args.Gym
|
||||||
|
let! attendance = client.GetGymAttendance id
|
||||||
|
|
||||||
|
if args.Terse then
|
||||||
|
System.Console.WriteLine attendance.TotalPeopleInGym
|
||||||
|
else
|
||||||
|
System.Console.WriteLine (string<GymAttendance> attendance)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
51
PureGym.App/LookupGym.fs
Normal file
51
PureGym.App/LookupGym.fs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
namespace PureGym.App
|
||||||
|
|
||||||
|
open Argu
|
||||||
|
open PureGym
|
||||||
|
|
||||||
|
type LookupGymArgsFragment =
|
||||||
|
| [<Unique>] Gym_Id of int
|
||||||
|
| [<Unique>] Gym_Name of string
|
||||||
|
|
||||||
|
interface IArgParserTemplate with
|
||||||
|
member s.Usage =
|
||||||
|
match s with
|
||||||
|
| LookupGymArgsFragment.Gym_Id _ -> "ID of the gym to look up, according to PureGym's internal mapping"
|
||||||
|
| LookupGymArgsFragment.Gym_Name _ -> "Name of a gym (best-guess fuzzy matching)"
|
||||||
|
|
||||||
|
type LookupGymArgs =
|
||||||
|
{
|
||||||
|
Creds : Auth
|
||||||
|
Gym : GymSelector
|
||||||
|
}
|
||||||
|
|
||||||
|
static member Parse
|
||||||
|
(auth : Auth)
|
||||||
|
(args : LookupGymArgsFragment ParseResults)
|
||||||
|
: Result<LookupGymArgs, ArguParseException>
|
||||||
|
=
|
||||||
|
let gym =
|
||||||
|
match args.TryGetResult LookupGymArgsFragment.Gym_Id, args.TryGetResult LookupGymArgsFragment.Gym_Name with
|
||||||
|
| Some _, Some _ -> failwith "--gym-id and --gym-name are mutually exclusive"
|
||||||
|
| None, None ->
|
||||||
|
System.Console.Error.WriteLine "No gym ID given and no gym named; assuming the user's home gym"
|
||||||
|
GymSelector.Home
|
||||||
|
| Some id, None -> GymSelector.Id id
|
||||||
|
| None, Some name -> GymSelector.Name name
|
||||||
|
|
||||||
|
{
|
||||||
|
Creds = auth
|
||||||
|
Gym = gym
|
||||||
|
}
|
||||||
|
|> Ok
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module LookupGym =
|
||||||
|
|
||||||
|
let run (args : LookupGymArgs) =
|
||||||
|
task {
|
||||||
|
let! client = Api.make args.Creds
|
||||||
|
let! s = client.GetGym 19
|
||||||
|
System.Console.WriteLine (string<Gym> s)
|
||||||
|
return 0
|
||||||
|
}
|
38
PureGym.App/MemberActivity.fs
Normal file
38
PureGym.App/MemberActivity.fs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
namespace PureGym.App
|
||||||
|
|
||||||
|
open Argu
|
||||||
|
open PureGym
|
||||||
|
|
||||||
|
type MemberActivityArgsFragment =
|
||||||
|
| MemberActivityArgsFragment of bool
|
||||||
|
|
||||||
|
interface IArgParserTemplate with
|
||||||
|
member s.Usage =
|
||||||
|
match s with
|
||||||
|
| MemberActivityArgsFragment _ -> "dummy argument: this subcommand has no args"
|
||||||
|
|
||||||
|
type MemberActivityArgs =
|
||||||
|
{
|
||||||
|
Creds : Auth
|
||||||
|
}
|
||||||
|
|
||||||
|
static member Parse
|
||||||
|
(auth : Auth)
|
||||||
|
(_ : MemberActivityArgsFragment ParseResults)
|
||||||
|
: Result<MemberActivityArgs, ArguParseException>
|
||||||
|
=
|
||||||
|
{
|
||||||
|
Creds = auth
|
||||||
|
}
|
||||||
|
|> Ok
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module MemberActivity =
|
||||||
|
|
||||||
|
let run (args : MemberActivityArgs) =
|
||||||
|
task {
|
||||||
|
let! client = Api.make args.Creds
|
||||||
|
let! activity = client.GetMemberActivity ()
|
||||||
|
System.Console.WriteLine (string<MemberActivity> activity)
|
||||||
|
return 0
|
||||||
|
}
|
118
PureGym.App/Program.fs
Normal file
118
PureGym.App/Program.fs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
namespace PureGym.App
|
||||||
|
|
||||||
|
open System
|
||||||
|
open Argu
|
||||||
|
|
||||||
|
type Subcommand =
|
||||||
|
| RequiresAuth of (PureGym.Auth -> ArgsCrate)
|
||||||
|
| NoAuth of ArgsCrate
|
||||||
|
|
||||||
|
module Program =
|
||||||
|
let subcommands =
|
||||||
|
[|
|
||||||
|
"auth", ("Get an authentication token", NoAuth (ArgsCrate.make GetTokenArg.Parse Authenticate.run))
|
||||||
|
|
||||||
|
"lookup-gym",
|
||||||
|
("Get information about the physical instantiation of a gym",
|
||||||
|
RequiresAuth (fun auth -> ArgsCrate.make (LookupGymArgs.Parse auth) LookupGym.run))
|
||||||
|
|
||||||
|
"fullness",
|
||||||
|
("Determine how full a gym is",
|
||||||
|
RequiresAuth (fun auth -> ArgsCrate.make (FullnessArgs.Parse auth) Fullness.run))
|
||||||
|
|
||||||
|
"activity",
|
||||||
|
("Get information about your gym usage",
|
||||||
|
RequiresAuth (fun auth -> ArgsCrate.make (MemberActivityArgs.Parse auth) MemberActivity.run))
|
||||||
|
|]
|
||||||
|
|> Map.ofArray
|
||||||
|
|
||||||
|
[<EntryPoint>]
|
||||||
|
let main argv =
|
||||||
|
// It looks like Argu doesn't really support the combination of subcommands and read-from-env-vars, so we just
|
||||||
|
// roll our own.
|
||||||
|
|
||||||
|
match Array.tryHead argv with
|
||||||
|
| None
|
||||||
|
| Some "--help" ->
|
||||||
|
subcommands.Keys
|
||||||
|
|> String.concat ","
|
||||||
|
|> eprintfn "Subcommands (try each with `--help`): %s"
|
||||||
|
|
||||||
|
0
|
||||||
|
|
||||||
|
| Some commandName ->
|
||||||
|
|
||||||
|
match Map.tryFind commandName subcommands with
|
||||||
|
| None ->
|
||||||
|
subcommands.Keys
|
||||||
|
|> String.concat ","
|
||||||
|
|> eprintfn "Unrecognised command '%s'. Subcommands (try each with `--help`): %s" commandName
|
||||||
|
|
||||||
|
127
|
||||||
|
|
||||||
|
| Some (_help, command) ->
|
||||||
|
|
||||||
|
let argv = Array.tail argv
|
||||||
|
let config = ConfigurationReader.FromEnvironmentVariables ()
|
||||||
|
|
||||||
|
let argv, command =
|
||||||
|
match command with
|
||||||
|
| RequiresAuth command ->
|
||||||
|
let authParser = ArgumentParser.Create<AuthArg> ()
|
||||||
|
|
||||||
|
let extractedAuthParse, helpRequested =
|
||||||
|
try
|
||||||
|
authParser.Parse (argv, config, raiseOnUsage = true, ignoreUnrecognized = true), false
|
||||||
|
with :? ArguParseException as e ->
|
||||||
|
if e.Message.StartsWith ("USAGE:", StringComparison.OrdinalIgnoreCase) then
|
||||||
|
authParser.Parse (argv, config, raiseOnUsage = false, ignoreUnrecognized = true), true
|
||||||
|
else
|
||||||
|
reraise ()
|
||||||
|
|
||||||
|
let authArgs, argv =
|
||||||
|
if helpRequested then
|
||||||
|
let subcommandArgs =
|
||||||
|
("--help" :: extractedAuthParse.GetResults AuthArg.Others) |> List.toArray
|
||||||
|
|
||||||
|
Unchecked.defaultof<_>, subcommandArgs
|
||||||
|
else
|
||||||
|
match AuthArg.Parse extractedAuthParse with
|
||||||
|
| Ok a -> a
|
||||||
|
| Error e -> Exception.reraiseWithOriginalStackTrace e
|
||||||
|
|
||||||
|
argv, command authArgs
|
||||||
|
| NoAuth command -> argv, command
|
||||||
|
|
||||||
|
{ new ArgsEvaluator<_> with
|
||||||
|
member _.Eval<'a, 'b when 'b :> IArgParserTemplate> (ofResult : ParseResults<'b> -> Result<'a, _>) run =
|
||||||
|
let parser = ArgumentParser.Create<'b> ()
|
||||||
|
|
||||||
|
let parsed =
|
||||||
|
try
|
||||||
|
parser.Parse (argv, config, raiseOnUsage = true) |> Ok
|
||||||
|
with :? ArguParseException as e ->
|
||||||
|
e.Message.Replace ("PureGym.App ", $"PureGym.App %s{commandName} ")
|
||||||
|
|> Console.Error.WriteLine
|
||||||
|
|
||||||
|
if e.Message.StartsWith ("USAGE:", StringComparison.OrdinalIgnoreCase) then
|
||||||
|
Error true
|
||||||
|
else
|
||||||
|
Error false
|
||||||
|
|
||||||
|
match parsed with
|
||||||
|
| Error false -> Error 127
|
||||||
|
| Error true -> Error 0
|
||||||
|
| Ok parsed ->
|
||||||
|
|
||||||
|
match ofResult parsed with
|
||||||
|
| Error e ->
|
||||||
|
e.Message.Replace ("PureGym.App ", $"PureGym.App %s{commandName} ")
|
||||||
|
|> Console.Error.WriteLine
|
||||||
|
|
||||||
|
Error 127
|
||||||
|
| Ok args ->
|
||||||
|
|
||||||
|
run args |> Ok
|
||||||
|
}
|
||||||
|
|> command.Apply
|
||||||
|
|> Result.cata (fun t -> t.Result) id
|
28
PureGym.App/PureGym.App.fsproj
Normal file
28
PureGym.App/PureGym.App.fsproj
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Result.fs" />
|
||||||
|
<Compile Include="Exception.fs" />
|
||||||
|
<Compile Include="ArgsCrate.fs" />
|
||||||
|
<Compile Include="AuthArg.fs" />
|
||||||
|
<Compile Include="Authenticate.fs" />
|
||||||
|
<Compile Include="Fullness.fs" />
|
||||||
|
<Compile Include="LookupGym.fs" />
|
||||||
|
<Compile Include="MemberActivity.fs" />
|
||||||
|
<Compile Include="Program.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PureGym\PureGym.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Argu" Version="6.1.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
9
PureGym.App/Result.fs
Normal file
9
PureGym.App/Result.fs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PureGym.App
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Result =
|
||||||
|
|
||||||
|
let cata<'ok, 'err, 'result> onOk onError (r : Result<'ok, 'err>) : 'result =
|
||||||
|
match r with
|
||||||
|
| Ok ok -> onOk ok
|
||||||
|
| Error e -> onError e
|
27
PureGym.Test/PureGym.Test.fsproj
Normal file
27
PureGym.Test/PureGym.Test.fsproj
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="TestSurface.fs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ApiSurface" Version="4.0.12" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
|
||||||
|
<PackageReference Include="NUnit" Version="3.13.3"/>
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2"/>
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.2.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PureGym\PureGym.fsproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
21
PureGym.Test/TestSurface.fs
Normal file
21
PureGym.Test/TestSurface.fs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace PureGym.Test
|
||||||
|
|
||||||
|
open PureGym
|
||||||
|
open NUnit.Framework
|
||||||
|
open ApiSurface
|
||||||
|
|
||||||
|
[<TestFixture>]
|
||||||
|
module TestSurface =
|
||||||
|
|
||||||
|
let assembly = typeof<Gym>.Assembly
|
||||||
|
|
||||||
|
[<Test>]
|
||||||
|
let ``Ensure API surface has not been modified`` () = ApiSurface.assertIdentical assembly
|
||||||
|
|
||||||
|
[<Test ; Explicit>]
|
||||||
|
let ``Update API surface`` () =
|
||||||
|
ApiSurface.writeAssemblyBaseline assembly
|
||||||
|
|
||||||
|
[<Test ; Explicit "This isn't done yet">]
|
||||||
|
let ``Ensure public API is fully documented`` () =
|
||||||
|
DocCoverage.assertFullyDocumented assembly
|
28
PureGym.sln
Normal file
28
PureGym.sln
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PureGym", "PureGym\PureGym.fsproj", "{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}"
|
||||||
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PureGym.App", "PureGym.App\PureGym.App.fsproj", "{B4848668-4CB2-4919-987D-8C95F865C81E}"
|
||||||
|
EndProject
|
||||||
|
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PureGym.Test", "PureGym.Test\PureGym.Test.fsproj", "{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BDF66CFC-5F2C-4AC4-A9FE-FB3B135E47FF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B4848668-4CB2-4919-987D-8C95F865C81E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B4848668-4CB2-4919-987D-8C95F865C81E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B4848668-4CB2-4919-987D-8C95F865C81E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B4848668-4CB2-4919-987D-8C95F865C81E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F09DF609-5F53-4BB3-BD64-DDB136CD4D2E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
272
PureGym/Api.fs
Normal file
272
PureGym/Api.fs
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
namespace PureGym
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Net.Http
|
||||||
|
open System.Text.Json.Serialization
|
||||||
|
open System.Threading.Tasks
|
||||||
|
open RestEase
|
||||||
|
|
||||||
|
/// Describes the opening hours of a given gym.
|
||||||
|
type GymOpeningHours =
|
||||||
|
{
|
||||||
|
/// If this is true, there should be no OpeningHours (but nothing enforces that).
|
||||||
|
IsAlwaysOpen : bool
|
||||||
|
/// This is a pretty unstructured list, which is in general not really parseable: it's human-readable only.
|
||||||
|
OpeningHours : string list
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Human-readable representation
|
||||||
|
override this.ToString () =
|
||||||
|
if this.IsAlwaysOpen then
|
||||||
|
"always open"
|
||||||
|
else
|
||||||
|
this.OpeningHours |> String.concat ", "
|
||||||
|
|
||||||
|
/// How a human can authenticate with a gym when they physically try to enter it
|
||||||
|
type GymAccessOptions =
|
||||||
|
{
|
||||||
|
/// This gym has PIN entry pads
|
||||||
|
PinAccess : bool
|
||||||
|
/// This gym has a QR code scanner. QR codes can be generated with the PureGym app.
|
||||||
|
QrCodeAccess : bool
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Human-readable representation
|
||||||
|
override this.ToString () =
|
||||||
|
$"Pin access: %c{Char.emoji this.PinAccess}; QR code access: %c{Char.emoji this.QrCodeAccess}"
|
||||||
|
|
||||||
|
/// Where a gym is on the Earth
|
||||||
|
type GymLocation =
|
||||||
|
{
|
||||||
|
/// Measured in degrees
|
||||||
|
Longitude : float
|
||||||
|
/// Measured in degrees
|
||||||
|
Latitude : float
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The postal address of a gym
|
||||||
|
type GymAddress =
|
||||||
|
{
|
||||||
|
/// E.g. "Canterbury Court"
|
||||||
|
[<JsonRequired>]
|
||||||
|
AddressLine1 : string
|
||||||
|
/// E.g. "Units 4, 4A, 5 And 5A"
|
||||||
|
AddressLine2 : string
|
||||||
|
/// E.g. "Kennington Park"
|
||||||
|
AddressLine3 : string
|
||||||
|
/// E.g. "LONDON"
|
||||||
|
[<JsonRequired>]
|
||||||
|
Town : string
|
||||||
|
County : string
|
||||||
|
/// E.g. "SW9 6DE"
|
||||||
|
[<JsonRequired>]
|
||||||
|
Postcode : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Human-readable statement of the address
|
||||||
|
override this.ToString () =
|
||||||
|
[
|
||||||
|
yield Some this.AddressLine1
|
||||||
|
yield this.AddressLine2 |> Option.ofObj
|
||||||
|
yield this.AddressLine3 |> Option.ofObj
|
||||||
|
match this.County with
|
||||||
|
| null -> yield Some $"%s{this.Town} %s{this.Postcode}"
|
||||||
|
| county ->
|
||||||
|
yield Some this.Town
|
||||||
|
yield Some $"%s{county} %s{this.Postcode}"
|
||||||
|
]
|
||||||
|
|> Seq.choose id
|
||||||
|
|> String.concat "\n"
|
||||||
|
|
||||||
|
/// Metadata about a physical gym
|
||||||
|
type Gym =
|
||||||
|
{
|
||||||
|
// The following fields are returned but are always null
|
||||||
|
// ReasonsToJoin : string
|
||||||
|
// VirtualTourUrl : Uri
|
||||||
|
// PersonalTrainersUrl : Uri
|
||||||
|
// WebViewUrl : Uri
|
||||||
|
// FloorPlanUrl : Uri
|
||||||
|
// StaffMembers : string
|
||||||
|
|
||||||
|
/// The name of this gym, e.g. "London Oval"
|
||||||
|
[<JsonRequired>]
|
||||||
|
Name : string
|
||||||
|
/// This gym's ID in the PureGym system, e.g. 19
|
||||||
|
[<JsonRequired>]
|
||||||
|
Id : int
|
||||||
|
/// I don't know what this status is. Please tell me if you know!
|
||||||
|
[<JsonRequired>]
|
||||||
|
Status : int
|
||||||
|
/// Postal address of this gym
|
||||||
|
[<JsonRequired>]
|
||||||
|
Address : GymAddress
|
||||||
|
/// Phone number of this gym, e.g. "+44 1234 567890"
|
||||||
|
[<JsonRequired>]
|
||||||
|
PhoneNumber : string
|
||||||
|
/// Contact email address for this gym's staff
|
||||||
|
[<JsonRequired>]
|
||||||
|
EmailAddress : string
|
||||||
|
/// When this gym is open
|
||||||
|
[<JsonRequired>]
|
||||||
|
GymOpeningHours : GymOpeningHours
|
||||||
|
/// How a human can physically authenticate when they physically enter this gym
|
||||||
|
[<JsonRequired>]
|
||||||
|
AccessOptions : GymAccessOptions
|
||||||
|
/// Where this gym is physically located
|
||||||
|
[<JsonRequired>]
|
||||||
|
Location : GymLocation
|
||||||
|
/// The IANA time zone this gym observes, e.g. "Europe/London"
|
||||||
|
[<JsonRequired>]
|
||||||
|
TimeZone : string
|
||||||
|
/// This is a date-time in the format yyyy-MM-ddTHH:mm:ss+01 Europe/London
|
||||||
|
ReopenDate : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Human-readable representation of the most important information about this gym
|
||||||
|
override this.ToString () =
|
||||||
|
$"""%s{this.Name} (%i{this.Id})
|
||||||
|
{this.Address}
|
||||||
|
%s{this.EmailAddress} %s{this.PhoneNumber}
|
||||||
|
Opening hours: %s{string<GymOpeningHours> this.GymOpeningHours}
|
||||||
|
%s{string<GymAccessOptions> this.AccessOptions}
|
||||||
|
"""
|
||||||
|
|
||||||
|
/// A human member of PureGym
|
||||||
|
type Member =
|
||||||
|
{
|
||||||
|
/// This member's ID. This is a fairly large number.
|
||||||
|
Id : int
|
||||||
|
/// No idea what this is - please tell me if you know!
|
||||||
|
CompoundMemberId : string
|
||||||
|
/// First name, e.g. "Patrick"
|
||||||
|
FirstName : string
|
||||||
|
/// Last name, e.g. "Stevens"
|
||||||
|
LastName : string
|
||||||
|
/// ID of the gym designated as this user's home gym. This is also the "Id" field of the appropriate Gym object.
|
||||||
|
HomeGymId : int
|
||||||
|
/// The name of the gym designated as this user's home gym. This is also the "Name" field of the appropriate
|
||||||
|
/// Gym object.
|
||||||
|
HomeGymName : string
|
||||||
|
/// This user's email address
|
||||||
|
EmailAddress : string
|
||||||
|
/// This user's gym access pin, probably 8 digits
|
||||||
|
GymAccessPin : string
|
||||||
|
/// This user's recorded date of birth
|
||||||
|
DateOfBirth : DateOnly
|
||||||
|
/// This user's phone number, human-readable
|
||||||
|
MobileNumber : string
|
||||||
|
/// This user's registered home postcode
|
||||||
|
Postcode : string
|
||||||
|
/// E.g. "Corporate"
|
||||||
|
MembershipName : string
|
||||||
|
MembershipLevel : int
|
||||||
|
SuspendedReason : int
|
||||||
|
MemberStatus : int
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Statistics for how many people are currently at a gym
|
||||||
|
type GymAttendance =
|
||||||
|
{
|
||||||
|
[<JsonRequired>]
|
||||||
|
Description : string
|
||||||
|
/// How many people are in the gym as of this statistics snapshot
|
||||||
|
[<JsonRequired>]
|
||||||
|
TotalPeopleInGym : int
|
||||||
|
/// How many people are in classes at the gym as of this statistics snapshot
|
||||||
|
[<JsonRequired>]
|
||||||
|
TotalPeopleInClasses : int
|
||||||
|
TotalPeopleSuffix : string
|
||||||
|
[<JsonRequired>]
|
||||||
|
IsApproximate : bool
|
||||||
|
/// When the query was received (I think)
|
||||||
|
AttendanceTime : DateTime
|
||||||
|
/// When the "total people in gym" snapshot was taken that is reported here
|
||||||
|
LastRefreshed : DateTime
|
||||||
|
/// When the "number of people in classes" snapshot was taken that is reported here
|
||||||
|
LastRefreshedPeopleInClasses : DateTime
|
||||||
|
/// Maximum capacity of the gym, or 0 if no listed capacity
|
||||||
|
MaximumCapacity : int
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Human-readable representation
|
||||||
|
override this.ToString () =
|
||||||
|
if not (Object.ReferenceEquals (this.TotalPeopleSuffix, null)) then
|
||||||
|
failwith $"Unexpectedly got Total People Suffix: %s{this.TotalPeopleSuffix}"
|
||||||
|
|
||||||
|
let capacity =
|
||||||
|
if this.MaximumCapacity = 0 then
|
||||||
|
""
|
||||||
|
else
|
||||||
|
$" out of %i{this.MaximumCapacity} maximum"
|
||||||
|
|
||||||
|
let classes =
|
||||||
|
if this.TotalPeopleInClasses = 0 then
|
||||||
|
""
|
||||||
|
else
|
||||||
|
$"\n%i{this.TotalPeopleInClasses} in classes"
|
||||||
|
|
||||||
|
$"""%i{this.TotalPeopleInGym} in gym%s{capacity} (is exact: %c{Char.emoji (not this.IsApproximate)})%s{classes}
|
||||||
|
Query made at %s{this.AttendanceTime.ToString ("s")}%s{this.AttendanceTime.ToString ("zzz")}
|
||||||
|
Snapshot correct as of %s{this.LastRefreshed.ToString ("s")}%s{this.LastRefreshed.ToString ("zzz")}
|
||||||
|
Classes info correct as of %s{this.LastRefreshedPeopleInClasses.ToString ("s")}%s{this.LastRefreshedPeopleInClasses.ToString ("zzz")}"""
|
||||||
|
|
||||||
|
/// The visit statistics for a particular human to a particular gym.
|
||||||
|
/// The semantics of this class are basically unknown.
|
||||||
|
type MemberActivity =
|
||||||
|
{
|
||||||
|
/// ??? semantics unknown; this was 2852 for me
|
||||||
|
[<JsonPropertyName "totalDuration">]
|
||||||
|
TotalDurationMinutes : int
|
||||||
|
[<JsonPropertyName "averageDuration">]
|
||||||
|
AverageDurationMinutes : int
|
||||||
|
TotalVisits : int
|
||||||
|
TotalClasses : int
|
||||||
|
IsEstimated : bool
|
||||||
|
LastRefreshed : DateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PureGym REST API. You probably want to instantiate one of these with `Api.make`.
|
||||||
|
[<Header("User-Agent", "PureGym/1523 CFNetwork/1312 Darwin/21.0.0")>]
|
||||||
|
type IPureGymApi =
|
||||||
|
/// Get the complete list of all gyms known to PureGym.
|
||||||
|
[<Get "v1/gyms/">]
|
||||||
|
abstract GetGyms : unit -> Task<Gym list>
|
||||||
|
|
||||||
|
/// Get information about the PureGym human whose credentials this client is authenticated with.
|
||||||
|
[<Get "v1/member">]
|
||||||
|
abstract GetMember : unit -> Task<Member>
|
||||||
|
|
||||||
|
/// Get information about how full the given gym currently is. The gym ID can be found from `GetGyms`.
|
||||||
|
[<Get "v1/gyms/{gym_id}/attendance">]
|
||||||
|
abstract GetGymAttendance : [<Path "gym_id">] gymId : int -> Task<GymAttendance>
|
||||||
|
|
||||||
|
/// Get information about a specific gym.
|
||||||
|
[<Get "v1/gyms/{gym_id}">]
|
||||||
|
abstract GetGym : [<Path "gym_id">] gymId : int -> Task<Gym>
|
||||||
|
|
||||||
|
/// Get information about the activities logged against the currently authenticated PureGym human.
|
||||||
|
[<Get "v1/member/activity">]
|
||||||
|
abstract GetMemberActivity : unit -> Task<MemberActivity>
|
||||||
|
|
||||||
|
/// Methods for interacting with the PureGym REST API.
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Api =
|
||||||
|
/// Create a REST client, authenticated as the specified user.
|
||||||
|
let make (auth : Auth) : IPureGymApi Task =
|
||||||
|
task {
|
||||||
|
let! token =
|
||||||
|
match auth with
|
||||||
|
| Auth.Token t -> Task.FromResult<_> t
|
||||||
|
| Auth.User cred -> AuthToken.get cred
|
||||||
|
|
||||||
|
let client = new HttpClient ()
|
||||||
|
client.BaseAddress <- Uri "https://capi.puregym.com/api"
|
||||||
|
|
||||||
|
client.DefaultRequestHeaders.Authorization <-
|
||||||
|
Headers.AuthenticationHeaderValue ("Bearer", token.AccessToken)
|
||||||
|
|
||||||
|
client.DefaultRequestHeaders.Add ("User-Agent", "PureGym/1523 CFNetwork/1312 Darwin/21.0.0")
|
||||||
|
|
||||||
|
return RestClient.For<IPureGymApi> client
|
||||||
|
}
|
97
PureGym/Auth.fs
Normal file
97
PureGym/Auth.fs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
namespace PureGym
|
||||||
|
|
||||||
|
open System
|
||||||
|
open System.Collections.Generic
|
||||||
|
open System.Net.Http
|
||||||
|
open System.Text.Json
|
||||||
|
open System.Text.Json.Serialization
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
|
// System.Text.Json does not support internal F# records as of .NET 8, presumably because it can't find the constructor.
|
||||||
|
// So we end up with this gruesome soup of C#.
|
||||||
|
// TODO(net8): make this internal
|
||||||
|
type AuthResponseRaw [<JsonConstructor>] (access_token : string, expires_in : int, token_type : string, scope : string)
|
||||||
|
=
|
||||||
|
member _.access_token = access_token
|
||||||
|
member _.expires_in = expires_in
|
||||||
|
member _.token_type = token_type
|
||||||
|
member _.scope = scope
|
||||||
|
|
||||||
|
/// A token which can be used to authenticate with the PureGym API.
|
||||||
|
type AuthToken =
|
||||||
|
{
|
||||||
|
/// A string which can be passed as the "Bearer" in an Authorization header
|
||||||
|
AccessToken : string
|
||||||
|
/// Time that this token expires, if known
|
||||||
|
ExpiryTime : DateTime option
|
||||||
|
}
|
||||||
|
|
||||||
|
static member internal Parse (response : AuthResponseRaw) : AuthToken =
|
||||||
|
if response.scope <> "pgcapi" then
|
||||||
|
failwithf $"unexpected scope: %s{response.scope}"
|
||||||
|
|
||||||
|
if response.token_type <> "Bearer" then
|
||||||
|
failwithf $"unexpected type: %s{response.token_type}"
|
||||||
|
|
||||||
|
{
|
||||||
|
AccessToken = response.access_token
|
||||||
|
ExpiryTime = Some (DateTime.Now + TimeSpan.FromSeconds response.expires_in)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authentication credentials as known to a human user
|
||||||
|
type UsernamePin =
|
||||||
|
{
|
||||||
|
/// The user's email address
|
||||||
|
Username : string
|
||||||
|
/// Eight-digit PIN
|
||||||
|
Pin : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Any way to authenticate with the PureGym API.
|
||||||
|
type Auth =
|
||||||
|
/// Authenticate with credentials which are known to the user
|
||||||
|
| User of UsernamePin
|
||||||
|
/// An AuthToken (that is, a bearer token)
|
||||||
|
| Token of AuthToken
|
||||||
|
|
||||||
|
/// Methods for constructing PureGym authentication tokens.
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module AuthToken =
|
||||||
|
/// Construct an AuthToken given that you already have a bearer token.
|
||||||
|
let ofBearerToken (token : string) : AuthToken =
|
||||||
|
{
|
||||||
|
AccessToken = token
|
||||||
|
ExpiryTime = None
|
||||||
|
}
|
||||||
|
|
||||||
|
let private options = JsonSerializerOptions (IncludeFields = true)
|
||||||
|
|
||||||
|
/// Get an AuthToken for the given user email address with the given eight-digit PureGym PIN.
|
||||||
|
let get (creds : UsernamePin) : Task<AuthToken> =
|
||||||
|
task {
|
||||||
|
use client = new HttpClient ()
|
||||||
|
client.BaseAddress <- Uri "https://auth.puregym.com"
|
||||||
|
client.DefaultRequestHeaders.Add ("User-Agent", "PureGym/1523 CFNetwork/1312 Darwin/21.0.0")
|
||||||
|
|
||||||
|
let request =
|
||||||
|
[
|
||||||
|
"grant_type", "password"
|
||||||
|
"username", creds.Username
|
||||||
|
"password", creds.Pin
|
||||||
|
"scope", "pgcapi"
|
||||||
|
"client_id", "ro.client"
|
||||||
|
]
|
||||||
|
|> List.map KeyValuePair
|
||||||
|
|
||||||
|
use content = new FormUrlEncodedContent (request)
|
||||||
|
let! response = client.PostAsync (Uri "https://auth.puregym.com/connect/token", content)
|
||||||
|
|
||||||
|
if response.IsSuccessStatusCode then
|
||||||
|
let! content = response.Content.ReadAsStreamAsync ()
|
||||||
|
let! response = JsonSerializer.DeserializeAsync<AuthResponseRaw> (content, options)
|
||||||
|
// let! response = JsonSerializer.DeserializeAsync<AuthResponseRaw> (content, options)
|
||||||
|
return AuthToken.Parse response
|
||||||
|
else
|
||||||
|
let! content = response.Content.ReadAsStringAsync ()
|
||||||
|
return failwithf $"bad status code: %+A{response.StatusCode}\n%s{content}"
|
||||||
|
}
|
48
PureGym/GymSelector.fs
Normal file
48
PureGym/GymSelector.fs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
namespace PureGym
|
||||||
|
|
||||||
|
open System.Threading.Tasks
|
||||||
|
open Fastenshtein
|
||||||
|
|
||||||
|
/// Identifies a gym, possibly non-uniquely and possibly ambiguously.
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
type GymSelector =
|
||||||
|
/// The ID of this gym, according to the PureGym internal mapping.
|
||||||
|
| Id of int
|
||||||
|
/// The user-specified name of this gym.
|
||||||
|
| Name of string
|
||||||
|
/// The home gym of the authenticated user.
|
||||||
|
| Home
|
||||||
|
|
||||||
|
/// Methods for manipulating GymSelector.
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module GymSelector =
|
||||||
|
|
||||||
|
/// Get the canonical PureGym ID for this user-specified gym.
|
||||||
|
let canonicalId (client : IPureGymApi) (gym : GymSelector) : int Task =
|
||||||
|
match gym with
|
||||||
|
| GymSelector.Home ->
|
||||||
|
task {
|
||||||
|
let! self = client.GetMember ()
|
||||||
|
return self.HomeGymId
|
||||||
|
}
|
||||||
|
| GymSelector.Id i -> Task.FromResult<_> i
|
||||||
|
| GymSelector.Name name ->
|
||||||
|
task {
|
||||||
|
let! allGyms = client.GetGyms ()
|
||||||
|
|
||||||
|
if allGyms.IsEmpty then
|
||||||
|
return failwith "PureGym API returned no gyms!"
|
||||||
|
else
|
||||||
|
let distance = Levenshtein (name.ToLowerInvariant ())
|
||||||
|
|
||||||
|
let bestDistance, bestGym =
|
||||||
|
allGyms
|
||||||
|
|> Seq.map (fun gym -> distance.DistanceFrom (gym.Name.ToLowerInvariant ()), gym)
|
||||||
|
|> Seq.sortBy fst
|
||||||
|
|> Seq.head
|
||||||
|
|
||||||
|
if bestDistance <> 0 then
|
||||||
|
System.Console.Error.WriteLine $"Autocorrected gym from %s{name} to %s{bestGym.Name}"
|
||||||
|
|
||||||
|
return bestGym.Id
|
||||||
|
}
|
24
PureGym/PureGym.fsproj
Normal file
24
PureGym/PureGym.fsproj
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="String.fs" />
|
||||||
|
<Compile Include="Auth.fs" />
|
||||||
|
<Compile Include="Api.fs" />
|
||||||
|
<Compile Include="GymSelector.fs" />
|
||||||
|
<EmbeddedResource Include="SurfaceBaseline.txt" />
|
||||||
|
<EmbeddedResource Include="version.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="RestEase" Version="1.6.4" />
|
||||||
|
<PackageReference Update="FSharp.Core" Version="6.0.0" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="7.0.3" />
|
||||||
|
<PackageReference Include="Fastenshtein" Version="1.0.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
5
PureGym/String.fs
Normal file
5
PureGym/String.fs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
namespace PureGym
|
||||||
|
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module internal Char =
|
||||||
|
let emoji bool = if bool then '✅' else '❌'
|
198
PureGym/SurfaceBaseline.txt
Normal file
198
PureGym/SurfaceBaseline.txt
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
PureGym.Api inherit obj
|
||||||
|
PureGym.Api.make [static method]: PureGym.Auth -> PureGym.IPureGymApi System.Threading.Tasks.Task
|
||||||
|
PureGym.Auth inherit obj, implements PureGym.Auth System.IEquatable, System.Collections.IStructuralEquatable, PureGym.Auth System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 2 cases
|
||||||
|
PureGym.Auth+Tags inherit obj
|
||||||
|
PureGym.Auth+Tags.Token [static field]: int = 1
|
||||||
|
PureGym.Auth+Tags.User [static field]: int = 0
|
||||||
|
PureGym.Auth+Token inherit PureGym.Auth
|
||||||
|
PureGym.Auth+Token.get_Item [method]: unit -> PureGym.AuthToken
|
||||||
|
PureGym.Auth+Token.Item [property]: [read-only] PureGym.AuthToken
|
||||||
|
PureGym.Auth+User inherit PureGym.Auth
|
||||||
|
PureGym.Auth+User.get_Item [method]: unit -> PureGym.UsernamePin
|
||||||
|
PureGym.Auth+User.Item [property]: [read-only] PureGym.UsernamePin
|
||||||
|
PureGym.Auth.get_IsToken [method]: unit -> bool
|
||||||
|
PureGym.Auth.get_IsUser [method]: unit -> bool
|
||||||
|
PureGym.Auth.get_Tag [method]: unit -> int
|
||||||
|
PureGym.Auth.IsToken [property]: [read-only] bool
|
||||||
|
PureGym.Auth.IsUser [property]: [read-only] bool
|
||||||
|
PureGym.Auth.NewToken [static method]: PureGym.AuthToken -> PureGym.Auth
|
||||||
|
PureGym.Auth.NewUser [static method]: PureGym.UsernamePin -> PureGym.Auth
|
||||||
|
PureGym.Auth.Tag [property]: [read-only] int
|
||||||
|
PureGym.AuthResponseRaw inherit obj
|
||||||
|
PureGym.AuthResponseRaw..ctor [constructor]: (string, int, string, string)
|
||||||
|
PureGym.AuthResponseRaw.access_token [property]: [read-only] string
|
||||||
|
PureGym.AuthResponseRaw.expires_in [property]: [read-only] int
|
||||||
|
PureGym.AuthResponseRaw.get_access_token [method]: unit -> string
|
||||||
|
PureGym.AuthResponseRaw.get_expires_in [method]: unit -> int
|
||||||
|
PureGym.AuthResponseRaw.get_scope [method]: unit -> string
|
||||||
|
PureGym.AuthResponseRaw.get_token_type [method]: unit -> string
|
||||||
|
PureGym.AuthResponseRaw.scope [property]: [read-only] string
|
||||||
|
PureGym.AuthResponseRaw.token_type [property]: [read-only] string
|
||||||
|
PureGym.AuthToken inherit obj, implements PureGym.AuthToken System.IEquatable, System.Collections.IStructuralEquatable, PureGym.AuthToken System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.AuthToken..ctor [constructor]: (string, System.DateTime option)
|
||||||
|
PureGym.AuthToken.AccessToken [property]: [read-only] string
|
||||||
|
PureGym.AuthToken.ExpiryTime [property]: [read-only] System.DateTime option
|
||||||
|
PureGym.AuthToken.get_AccessToken [method]: unit -> string
|
||||||
|
PureGym.AuthToken.get_ExpiryTime [method]: unit -> System.DateTime option
|
||||||
|
PureGym.AuthTokenModule inherit obj
|
||||||
|
PureGym.AuthTokenModule.get [static method]: PureGym.UsernamePin -> PureGym.AuthToken System.Threading.Tasks.Task
|
||||||
|
PureGym.AuthTokenModule.ofBearerToken [static method]: string -> PureGym.AuthToken
|
||||||
|
PureGym.Gym inherit obj, implements PureGym.Gym System.IEquatable, System.Collections.IStructuralEquatable, PureGym.Gym System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.Gym..ctor [constructor]: (string, int, int, PureGym.GymAddress, string, string, PureGym.GymOpeningHours, PureGym.GymAccessOptions, PureGym.GymLocation, string, string)
|
||||||
|
PureGym.Gym.AccessOptions [property]: [read-only] PureGym.GymAccessOptions
|
||||||
|
PureGym.Gym.Address [property]: [read-only] PureGym.GymAddress
|
||||||
|
PureGym.Gym.EmailAddress [property]: [read-only] string
|
||||||
|
PureGym.Gym.get_AccessOptions [method]: unit -> PureGym.GymAccessOptions
|
||||||
|
PureGym.Gym.get_Address [method]: unit -> PureGym.GymAddress
|
||||||
|
PureGym.Gym.get_EmailAddress [method]: unit -> string
|
||||||
|
PureGym.Gym.get_GymOpeningHours [method]: unit -> PureGym.GymOpeningHours
|
||||||
|
PureGym.Gym.get_Id [method]: unit -> int
|
||||||
|
PureGym.Gym.get_Location [method]: unit -> PureGym.GymLocation
|
||||||
|
PureGym.Gym.get_Name [method]: unit -> string
|
||||||
|
PureGym.Gym.get_PhoneNumber [method]: unit -> string
|
||||||
|
PureGym.Gym.get_ReopenDate [method]: unit -> string
|
||||||
|
PureGym.Gym.get_Status [method]: unit -> int
|
||||||
|
PureGym.Gym.get_TimeZone [method]: unit -> string
|
||||||
|
PureGym.Gym.GymOpeningHours [property]: [read-only] PureGym.GymOpeningHours
|
||||||
|
PureGym.Gym.Id [property]: [read-only] int
|
||||||
|
PureGym.Gym.Location [property]: [read-only] PureGym.GymLocation
|
||||||
|
PureGym.Gym.Name [property]: [read-only] string
|
||||||
|
PureGym.Gym.PhoneNumber [property]: [read-only] string
|
||||||
|
PureGym.Gym.ReopenDate [property]: [read-only] string
|
||||||
|
PureGym.Gym.Status [property]: [read-only] int
|
||||||
|
PureGym.Gym.TimeZone [property]: [read-only] string
|
||||||
|
PureGym.GymAccessOptions inherit obj, implements PureGym.GymAccessOptions System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymAccessOptions System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.GymAccessOptions..ctor [constructor]: (bool, bool)
|
||||||
|
PureGym.GymAccessOptions.get_PinAccess [method]: unit -> bool
|
||||||
|
PureGym.GymAccessOptions.get_QrCodeAccess [method]: unit -> bool
|
||||||
|
PureGym.GymAccessOptions.PinAccess [property]: [read-only] bool
|
||||||
|
PureGym.GymAccessOptions.QrCodeAccess [property]: [read-only] bool
|
||||||
|
PureGym.GymAddress inherit obj, implements PureGym.GymAddress System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymAddress System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.GymAddress..ctor [constructor]: (string, string, string, string, string, string)
|
||||||
|
PureGym.GymAddress.AddressLine1 [property]: [read-only] string
|
||||||
|
PureGym.GymAddress.AddressLine2 [property]: [read-only] string
|
||||||
|
PureGym.GymAddress.AddressLine3 [property]: [read-only] string
|
||||||
|
PureGym.GymAddress.County [property]: [read-only] string
|
||||||
|
PureGym.GymAddress.get_AddressLine1 [method]: unit -> string
|
||||||
|
PureGym.GymAddress.get_AddressLine2 [method]: unit -> string
|
||||||
|
PureGym.GymAddress.get_AddressLine3 [method]: unit -> string
|
||||||
|
PureGym.GymAddress.get_County [method]: unit -> string
|
||||||
|
PureGym.GymAddress.get_Postcode [method]: unit -> string
|
||||||
|
PureGym.GymAddress.get_Town [method]: unit -> string
|
||||||
|
PureGym.GymAddress.Postcode [property]: [read-only] string
|
||||||
|
PureGym.GymAddress.Town [property]: [read-only] string
|
||||||
|
PureGym.GymAttendance inherit obj, implements PureGym.GymAttendance System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymAttendance System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.GymAttendance..ctor [constructor]: (string, int, int, string, bool, System.DateTime, System.DateTime, System.DateTime, int)
|
||||||
|
PureGym.GymAttendance.AttendanceTime [property]: [read-only] System.DateTime
|
||||||
|
PureGym.GymAttendance.Description [property]: [read-only] string
|
||||||
|
PureGym.GymAttendance.get_AttendanceTime [method]: unit -> System.DateTime
|
||||||
|
PureGym.GymAttendance.get_Description [method]: unit -> string
|
||||||
|
PureGym.GymAttendance.get_IsApproximate [method]: unit -> bool
|
||||||
|
PureGym.GymAttendance.get_LastRefreshed [method]: unit -> System.DateTime
|
||||||
|
PureGym.GymAttendance.get_LastRefreshedPeopleInClasses [method]: unit -> System.DateTime
|
||||||
|
PureGym.GymAttendance.get_MaximumCapacity [method]: unit -> int
|
||||||
|
PureGym.GymAttendance.get_TotalPeopleInClasses [method]: unit -> int
|
||||||
|
PureGym.GymAttendance.get_TotalPeopleInGym [method]: unit -> int
|
||||||
|
PureGym.GymAttendance.get_TotalPeopleSuffix [method]: unit -> string
|
||||||
|
PureGym.GymAttendance.IsApproximate [property]: [read-only] bool
|
||||||
|
PureGym.GymAttendance.LastRefreshed [property]: [read-only] System.DateTime
|
||||||
|
PureGym.GymAttendance.LastRefreshedPeopleInClasses [property]: [read-only] System.DateTime
|
||||||
|
PureGym.GymAttendance.MaximumCapacity [property]: [read-only] int
|
||||||
|
PureGym.GymAttendance.TotalPeopleInClasses [property]: [read-only] int
|
||||||
|
PureGym.GymAttendance.TotalPeopleInGym [property]: [read-only] int
|
||||||
|
PureGym.GymAttendance.TotalPeopleSuffix [property]: [read-only] string
|
||||||
|
PureGym.GymLocation inherit obj, implements PureGym.GymLocation System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymLocation System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.GymLocation..ctor [constructor]: (float, float)
|
||||||
|
PureGym.GymLocation.get_Latitude [method]: unit -> float
|
||||||
|
PureGym.GymLocation.get_Longitude [method]: unit -> float
|
||||||
|
PureGym.GymLocation.Latitude [property]: [read-only] float
|
||||||
|
PureGym.GymLocation.Longitude [property]: [read-only] float
|
||||||
|
PureGym.GymOpeningHours inherit obj, implements PureGym.GymOpeningHours System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymOpeningHours System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.GymOpeningHours..ctor [constructor]: (bool, string list)
|
||||||
|
PureGym.GymOpeningHours.get_IsAlwaysOpen [method]: unit -> bool
|
||||||
|
PureGym.GymOpeningHours.get_OpeningHours [method]: unit -> string list
|
||||||
|
PureGym.GymOpeningHours.IsAlwaysOpen [property]: [read-only] bool
|
||||||
|
PureGym.GymOpeningHours.OpeningHours [property]: [read-only] string list
|
||||||
|
PureGym.GymSelector inherit obj, implements PureGym.GymSelector System.IEquatable, System.Collections.IStructuralEquatable, PureGym.GymSelector System.IComparable, System.IComparable, System.Collections.IStructuralComparable - union type with 3 cases
|
||||||
|
PureGym.GymSelector+Id inherit PureGym.GymSelector
|
||||||
|
PureGym.GymSelector+Id.get_Item [method]: unit -> int
|
||||||
|
PureGym.GymSelector+Id.Item [property]: [read-only] int
|
||||||
|
PureGym.GymSelector+Name inherit PureGym.GymSelector
|
||||||
|
PureGym.GymSelector+Name.get_Item [method]: unit -> string
|
||||||
|
PureGym.GymSelector+Name.Item [property]: [read-only] string
|
||||||
|
PureGym.GymSelector+Tags inherit obj
|
||||||
|
PureGym.GymSelector+Tags.Home [static field]: int = 2
|
||||||
|
PureGym.GymSelector+Tags.Id [static field]: int = 0
|
||||||
|
PureGym.GymSelector+Tags.Name [static field]: int = 1
|
||||||
|
PureGym.GymSelector.get_Home [static method]: unit -> PureGym.GymSelector
|
||||||
|
PureGym.GymSelector.get_IsHome [method]: unit -> bool
|
||||||
|
PureGym.GymSelector.get_IsId [method]: unit -> bool
|
||||||
|
PureGym.GymSelector.get_IsName [method]: unit -> bool
|
||||||
|
PureGym.GymSelector.get_Tag [method]: unit -> int
|
||||||
|
PureGym.GymSelector.Home [static property]: [read-only] PureGym.GymSelector
|
||||||
|
PureGym.GymSelector.IsHome [property]: [read-only] bool
|
||||||
|
PureGym.GymSelector.IsId [property]: [read-only] bool
|
||||||
|
PureGym.GymSelector.IsName [property]: [read-only] bool
|
||||||
|
PureGym.GymSelector.NewId [static method]: int -> PureGym.GymSelector
|
||||||
|
PureGym.GymSelector.NewName [static method]: string -> PureGym.GymSelector
|
||||||
|
PureGym.GymSelector.Tag [property]: [read-only] int
|
||||||
|
PureGym.GymSelectorModule inherit obj
|
||||||
|
PureGym.GymSelectorModule.canonicalId [static method]: PureGym.IPureGymApi -> PureGym.GymSelector -> int System.Threading.Tasks.Task
|
||||||
|
PureGym.IPureGymApi - interface with 5 member(s)
|
||||||
|
PureGym.IPureGymApi.GetGym [method]: int -> PureGym.Gym System.Threading.Tasks.Task
|
||||||
|
PureGym.IPureGymApi.GetGymAttendance [method]: int -> PureGym.GymAttendance System.Threading.Tasks.Task
|
||||||
|
PureGym.IPureGymApi.GetGyms [method]: unit -> PureGym.Gym list System.Threading.Tasks.Task
|
||||||
|
PureGym.IPureGymApi.GetMember [method]: unit -> PureGym.Member System.Threading.Tasks.Task
|
||||||
|
PureGym.IPureGymApi.GetMemberActivity [method]: unit -> PureGym.MemberActivity System.Threading.Tasks.Task
|
||||||
|
PureGym.Member inherit obj, implements PureGym.Member System.IEquatable, System.Collections.IStructuralEquatable, PureGym.Member System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.Member..ctor [constructor]: (int, string, string, string, int, string, string, string, System.DateOnly, string, string, string, int, int, int)
|
||||||
|
PureGym.Member.CompoundMemberId [property]: [read-only] string
|
||||||
|
PureGym.Member.DateOfBirth [property]: [read-only] System.DateOnly
|
||||||
|
PureGym.Member.EmailAddress [property]: [read-only] string
|
||||||
|
PureGym.Member.FirstName [property]: [read-only] string
|
||||||
|
PureGym.Member.get_CompoundMemberId [method]: unit -> string
|
||||||
|
PureGym.Member.get_DateOfBirth [method]: unit -> System.DateOnly
|
||||||
|
PureGym.Member.get_EmailAddress [method]: unit -> string
|
||||||
|
PureGym.Member.get_FirstName [method]: unit -> string
|
||||||
|
PureGym.Member.get_GymAccessPin [method]: unit -> string
|
||||||
|
PureGym.Member.get_HomeGymId [method]: unit -> int
|
||||||
|
PureGym.Member.get_HomeGymName [method]: unit -> string
|
||||||
|
PureGym.Member.get_Id [method]: unit -> int
|
||||||
|
PureGym.Member.get_LastName [method]: unit -> string
|
||||||
|
PureGym.Member.get_MembershipLevel [method]: unit -> int
|
||||||
|
PureGym.Member.get_MembershipName [method]: unit -> string
|
||||||
|
PureGym.Member.get_MemberStatus [method]: unit -> int
|
||||||
|
PureGym.Member.get_MobileNumber [method]: unit -> string
|
||||||
|
PureGym.Member.get_Postcode [method]: unit -> string
|
||||||
|
PureGym.Member.get_SuspendedReason [method]: unit -> int
|
||||||
|
PureGym.Member.GymAccessPin [property]: [read-only] string
|
||||||
|
PureGym.Member.HomeGymId [property]: [read-only] int
|
||||||
|
PureGym.Member.HomeGymName [property]: [read-only] string
|
||||||
|
PureGym.Member.Id [property]: [read-only] int
|
||||||
|
PureGym.Member.LastName [property]: [read-only] string
|
||||||
|
PureGym.Member.MembershipLevel [property]: [read-only] int
|
||||||
|
PureGym.Member.MembershipName [property]: [read-only] string
|
||||||
|
PureGym.Member.MemberStatus [property]: [read-only] int
|
||||||
|
PureGym.Member.MobileNumber [property]: [read-only] string
|
||||||
|
PureGym.Member.Postcode [property]: [read-only] string
|
||||||
|
PureGym.Member.SuspendedReason [property]: [read-only] int
|
||||||
|
PureGym.MemberActivity inherit obj, implements PureGym.MemberActivity System.IEquatable, System.Collections.IStructuralEquatable, PureGym.MemberActivity System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.MemberActivity..ctor [constructor]: (int, int, int, int, bool, System.DateTime)
|
||||||
|
PureGym.MemberActivity.AverageDurationMinutes [property]: [read-only] int
|
||||||
|
PureGym.MemberActivity.get_AverageDurationMinutes [method]: unit -> int
|
||||||
|
PureGym.MemberActivity.get_IsEstimated [method]: unit -> bool
|
||||||
|
PureGym.MemberActivity.get_LastRefreshed [method]: unit -> System.DateTime
|
||||||
|
PureGym.MemberActivity.get_TotalClasses [method]: unit -> int
|
||||||
|
PureGym.MemberActivity.get_TotalDurationMinutes [method]: unit -> int
|
||||||
|
PureGym.MemberActivity.get_TotalVisits [method]: unit -> int
|
||||||
|
PureGym.MemberActivity.IsEstimated [property]: [read-only] bool
|
||||||
|
PureGym.MemberActivity.LastRefreshed [property]: [read-only] System.DateTime
|
||||||
|
PureGym.MemberActivity.TotalClasses [property]: [read-only] int
|
||||||
|
PureGym.MemberActivity.TotalDurationMinutes [property]: [read-only] int
|
||||||
|
PureGym.MemberActivity.TotalVisits [property]: [read-only] int
|
||||||
|
PureGym.UsernamePin inherit obj, implements PureGym.UsernamePin System.IEquatable, System.Collections.IStructuralEquatable, PureGym.UsernamePin System.IComparable, System.IComparable, System.Collections.IStructuralComparable
|
||||||
|
PureGym.UsernamePin..ctor [constructor]: (string, string)
|
||||||
|
PureGym.UsernamePin.get_Pin [method]: unit -> string
|
||||||
|
PureGym.UsernamePin.get_Username [method]: unit -> string
|
||||||
|
PureGym.UsernamePin.Pin [property]: [read-only] string
|
||||||
|
PureGym.UsernamePin.Username [property]: [read-only] string
|
7
PureGym/version.json
Normal file
7
PureGym/version.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"publicReleaseRefSpec": [
|
||||||
|
"^refs/heads/main$"
|
||||||
|
],
|
||||||
|
"pathFilters": null
|
||||||
|
}
|
30
README.md
Normal file
30
README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Unofficial PureGym client
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*With thanks to [Tom Hollingsworth](https://github.com/2t6h/puregym-attendance/blob/64dcd830bd874dc0150c7767f5cc6c75ed0b9dad/puregym.py).*
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
* With Nix: `nix run` (you can refer to this flake).
|
||||||
|
* Manually: `git clone` and then `dotnet run --project PureGym.App/PureGym.App.fsproj`.
|
||||||
|
|
||||||
|
(Something is up on Darwin: `nix run` currently produces an executable which dies instantly.
|
||||||
|
Workaround: `nix build` and then `nix develop --command dotnet exec ./result/lib/puregym/PureGym.App.dll`.)
|
||||||
|
|
||||||
|
The available subcommands can be viewed in the `subcommands` map defined in [Program.fs](./PureGym.App/Program.fs).
|
||||||
|
As of this writing, the following are implemented:
|
||||||
|
|
||||||
|
* `activity`: get the logged-in user's activity stats. I have no idea what the semantics of these numbers are!
|
||||||
|
* `fullness`: determine how full a given gym is right now.
|
||||||
|
* `lookup-gym`: give information about the gym's physical instantiation (e.g. its address).
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
You can authenticate with your PureGym email address and PIN combination, or (probably better) you can call `PureGym.App auth` to obtain a token.
|
||||||
|
Use this token in subsequent commands by setting the `PUREGYM_BEARER_TOKEN` environment variable or supplying it as `--bearer-token`.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
* The REST client is at [Api.fs](./PureGym/Api.fs).
|
||||||
|
* The standalone application is at [PureGym.App](./PureGym.App). It uses a ghastly mix of hand-rolled argument parsing and [Argu](https://fsprojects.github.io/Argu/), because Argu does not *quite* want to do what I want an argument parser to do.
|
60
flake.lock
generated
Normal file
60
flake.lock
generated
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1694529238,
|
||||||
|
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696981262,
|
||||||
|
"narHash": "sha256-YaCOjdqhbjBeyMjxlgFWt4XD/b9pGKWURgS3uEwNLtc=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a2b87a4f66f309d2f4b789fd0457f5fc5db0a9a6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
114
flake.nix
Normal file
114
flake.nix
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs";
|
||||||
|
flake-utils = {
|
||||||
|
url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = inputs @ {
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
projectFile = "./PureGym.App/PureGym.App.fsproj";
|
||||||
|
testProjectFile = "./PureGym.Test/PureGym.Test.fsproj";
|
||||||
|
pname = "puregym";
|
||||||
|
dotnet-sdk = pkgs.dotnet-sdk_7;
|
||||||
|
dotnet-runtime = pkgs.dotnetCorePackages.runtime_7_0;
|
||||||
|
version = "0.1";
|
||||||
|
dotnetTool = toolName: toolVersion: sha256:
|
||||||
|
pkgs.stdenvNoCC.mkDerivation rec {
|
||||||
|
name = toolName;
|
||||||
|
version = toolVersion;
|
||||||
|
nativeBuildInputs = [pkgs.makeWrapper];
|
||||||
|
src = pkgs.fetchNuGet {
|
||||||
|
pname = name;
|
||||||
|
version = version;
|
||||||
|
sha256 = sha256;
|
||||||
|
installPhase = ''mkdir -p $out/bin && cp -r tools/net6.0/any/* $out/bin'';
|
||||||
|
};
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p "$out/lib"
|
||||||
|
cp -r ./bin/* "$out/lib"
|
||||||
|
makeWrapper "${dotnet-runtime}/bin/dotnet" "$out/bin/${name}" --add-flags "$out/lib/${name}.dll"
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
fantomas = dotnetTool "fantomas" (builtins.fromJSON (builtins.readFile ./.config/dotnet-tools.json)).tools.fantomas.version "sha256-83RodORaC3rkYfbFMHsYLEtl0+8+akZXcKoSJdgwuUo=";
|
||||||
|
in {
|
||||||
|
packages = {
|
||||||
|
fantomas = fantomas;
|
||||||
|
fetchDeps = let
|
||||||
|
flags = [];
|
||||||
|
runtimeIds = ["win-x64"] ++ map (system: pkgs.dotnetCorePackages.systemToDotnetRid system) dotnet-sdk.meta.platforms;
|
||||||
|
in
|
||||||
|
pkgs.writeShellScriptBin "fetch-${pname}-deps" (builtins.readFile (pkgs.substituteAll {
|
||||||
|
src = ./nix/fetchDeps.sh;
|
||||||
|
pname = pname;
|
||||||
|
binPath = pkgs.lib.makeBinPath [pkgs.coreutils dotnet-sdk (pkgs.nuget-to-nix.override {inherit dotnet-sdk;})];
|
||||||
|
projectFiles = toString (pkgs.lib.toList projectFile);
|
||||||
|
testProjectFiles = toString (pkgs.lib.toList testProjectFile);
|
||||||
|
rids = pkgs.lib.concatStringsSep "\" \"" runtimeIds;
|
||||||
|
packages = dotnet-sdk.packages;
|
||||||
|
storeSrc = pkgs.srcOnly {
|
||||||
|
src = ./.;
|
||||||
|
pname = pname;
|
||||||
|
version = version;
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
default = pkgs.buildDotnetModule {
|
||||||
|
pname = pname;
|
||||||
|
name = "puregym";
|
||||||
|
version = version;
|
||||||
|
src = ./.;
|
||||||
|
projectFile = projectFile;
|
||||||
|
nugetDeps = ./nix/deps.nix;
|
||||||
|
doCheck = true;
|
||||||
|
dotnet-sdk = dotnet-sdk;
|
||||||
|
dotnet-runtime = dotnet-runtime;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
apps = {
|
||||||
|
default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${self.packages.${system}.default}/bin/PureGym.App";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs =
|
||||||
|
[pkgs.alejandra pkgs.dotnet-sdk_7 pkgs.python3 pkgs.nodePackages.markdown-link-check]
|
||||||
|
++ (
|
||||||
|
if pkgs.stdenv.isDarwin
|
||||||
|
then [pkgs.darwin.apple_sdk.frameworks.CoreServices]
|
||||||
|
else []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
checks = {
|
||||||
|
alejandra = pkgs.stdenvNoCC.mkDerivation {
|
||||||
|
name = "alejandra-check";
|
||||||
|
src = ./.;
|
||||||
|
checkPhase = ''
|
||||||
|
${pkgs.alejandra}/bin/alejandra --check .
|
||||||
|
'';
|
||||||
|
installPhase = "mkdir $out";
|
||||||
|
dontBuild = true;
|
||||||
|
doCheck = true;
|
||||||
|
};
|
||||||
|
fantomas = pkgs.stdenvNoCC.mkDerivation {
|
||||||
|
name = "fantomas-check";
|
||||||
|
src = ./.;
|
||||||
|
checkPhase = ''
|
||||||
|
${fantomas}/bin/fantomas --check .
|
||||||
|
'';
|
||||||
|
installPhase = "mkdir $out";
|
||||||
|
dontBuild = true;
|
||||||
|
doCheck = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
364
nix/deps.nix
Normal file
364
nix/deps.nix
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# This file was automatically generated by passthru.fetch-deps.
|
||||||
|
# Please don't edit it manually, your changes might get overwritten!
|
||||||
|
{fetchNuGet}: [
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Fastenshtein";
|
||||||
|
version = "1.0.0.8";
|
||||||
|
sha256 = "1rvw27rz7qb2n68i0jvvcr224fcpy5yzzxaj1bp89jw41cpdabp2";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Argu";
|
||||||
|
version = "6.1.1";
|
||||||
|
sha256 = "1v996g0760qhiys2ahdpnvkldaxr2jn5f1falf789glnk4a6f3xl";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Configuration.ConfigurationManager";
|
||||||
|
version = "4.4.0";
|
||||||
|
sha256 = "1hjgmz47v5229cbzd2pwz2h0dkq78lb2wp9grx8qr72pb5i0dk7v";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "fantomas";
|
||||||
|
version = "6.2.0";
|
||||||
|
sha256 = "sha256-83RodORaC3rkYfbFMHsYLEtl0+8+akZXcKoSJdgwuUo=";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "ApiSurface";
|
||||||
|
version = "4.0.12";
|
||||||
|
sha256 = "0v56sv4cz8bgrfqjjg0q96619qs9dvvi0a6lp7hzz2mi82i1inmq";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "coverlet.collector";
|
||||||
|
version = "3.2.0";
|
||||||
|
sha256 = "1qxpv8v10p5wn162lzdm193gdl6c5f81zadj8h889dprlnj3g8yr";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "FSharp.Core";
|
||||||
|
version = "6.0.0";
|
||||||
|
sha256 = "1hjhvr39c1vpgrdmf8xln5q86424fqkvy9nirkr29vl2461d2039";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "FSharp.Core";
|
||||||
|
version = "7.0.400";
|
||||||
|
sha256 = "1pl6iqqcpm9djfn7f6ms5j1xbcyz00nb808qd6pmsjrnylflalgp";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Ref";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "0fqpl1fr213b4fb3c6xw3fy6669yxqcp1bzcnayw80yrskw8lpxs";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.linux-arm64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "1xvqqc7bzj764g3scp0saqxlfiv866crgi8chz57vhjp9sgd61jw";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.linux-arm64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "0hmsqy4yc3023mcp5rg0h59yv3f8cnjhxw1g4i8md67vm5y04lfv";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.linux-x64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "1gcv99y295fnhy12fyx8wqvbhbj6mz8p5bm66ppwdxb3zykjg2l8";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.linux-x64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "18sk9wka8z5354ca77q43hi0615yjssdjbyi0hqq92w6zmg43vgc";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.osx-arm64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "1ib0x1w33wqy7lgzjf14dvgx981xpjffjqd800d7wgxisgmakrmr";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.osx-arm64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "1j0zbd4rmmd3ylgixsvyj145g2r6px6b9d9k4yxxg6d61x90c165";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.osx-x64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "026r38a7by7wdfd3virjdaah3y2sjjmnabgf5l25vdnwpwc7c31d";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.osx-x64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "0wxw7vgygg6hqzq479n0pfjizr69wq7ja03a0qh8bma8b9q2mn6f";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.win-x64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "0ygdqsd312kqpykwb0k2942n45q1w3yn1nia6m1ahf7b74926qb5";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.AspNetCore.App.Runtime.win-x64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "05ywwfn5lzx6y999f7gwmablkxi2zvska4sg20ihmjzp3xakcmk0";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.CodeCoverage";
|
||||||
|
version = "17.6.0";
|
||||||
|
sha256 = "02s98d8nwz5mg4mymcr86qdamy71a29g2091xg452czmd3s3x2di";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NET.Test.Sdk";
|
||||||
|
version = "17.6.0";
|
||||||
|
sha256 = "1bnwpwg7k72z06027ip4yi222863r8sv14ck9nj8h64ckiw2r256";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Host.linux-arm64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "0gri1gqznm5c8fsb6spqb3j88a3b0br0iy50y66fh4hz9wc4fwzm";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Host.linux-arm64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "03nkxjn4wq30rw0163rqi8sngfxmcvwgm0wg7sgyb1cdh0q1ai68";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Host.linux-x64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "0k1i74wn6j7nq0bd8m6jrpl65wda6qc9pglppvz4ybk0n2ab1rbi";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Host.linux-x64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "12hh69sr4wf8sjcw3q71vky51sn854ffahbq6rgz3njzvbvc0dbj";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Host.osx-arm64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "0166gwarhhnary19lf80ff33bkx00mkm24f17bc8j6v7g3a7zvq6";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Host.osx-x64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "038bjwk201p2kzs3jflrkhlnszf7cwalafq0nvs2v8bp7jlnx5ib";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Host.osx-x64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "1j1k735gkwba93n5yck87wppfpsbny979hppcygwrk81myf3fv03";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Host.win-x64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "1bjy3zmrmaq97xp0f3nzs3ax330ji632avrfpg8xz4vc5p8s1xpc";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Host.win-x64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "0ifshdx19bgnbgynbk6iy6gybnxmp63nylrn7068x66hvcavh7kh";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Ref";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "0km8184kma8kgz7iyl3j6apj1n7vskzdhzmq3myy3y36ysqrb4wf";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.linux-arm64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "01gbl9dgky4h7ijxryz3527l39v23lkcvk4fs4w91ra4pris2n8p";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.linux-arm64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "1gzwc96fs222ddia0k1924cn7gxm2a4anqgcxhmavx56x76wsy6f";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.linux-x64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "09gfqdxbh36bjx20fw9k94b9qa9bwffhrq0ldwn834mx31bgrfs8";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.linux-x64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "0vxza49wwiia0d3m887yiaprp3xnax2bgzhj5bf080b4ayapzkf9";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.osx-arm64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "1x7wclv93q8wp7rip5nwnsxbqcami92yilvzbp0yn42ddkw177ds";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.osx-arm64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "15b62hxrpfy19xvyxlyligixxpa9sysfgi47xi4imx5055fhwphh";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.osx-x64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "1sq1ygsrpv2sl85wrs8382wgkjic0zylaj1y8kcvhczcmkpk3wr5";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.osx-x64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "018qf23b0jixfh3fm74zqaakk01qx6yq21gk2mdn68b0xhnvlzma";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.win-x64";
|
||||||
|
version = "6.0.22";
|
||||||
|
sha256 = "1nn254xv1hi5c4rg38fbfkln3031vv545lv9f4df31i8c1yfzz24";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.App.Runtime.win-x64";
|
||||||
|
version = "7.0.11";
|
||||||
|
sha256 = "12xmw2kcpf5rh8sv4y0mqzp917f7q8g4mfh5navqw4jmnxyb26qq";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.Platforms";
|
||||||
|
version = "1.1.0";
|
||||||
|
sha256 = "08vh1r12g6ykjygq5d3vq09zylgb84l63k49jc4v8faw9g93iqqm";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.NETCore.Platforms";
|
||||||
|
version = "2.0.0";
|
||||||
|
sha256 = "1fk2fk2639i7nzy58m9dvpdnzql4vb8yl8vr19r2fp8lmj9w2jr0";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.TestPlatform.ObjectModel";
|
||||||
|
version = "17.6.0";
|
||||||
|
sha256 = "1rz22chnis11dwjrqrcvvmfw80fi2a7756a7ahwy6jlnr250zr61";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Microsoft.TestPlatform.TestHost";
|
||||||
|
version = "17.6.0";
|
||||||
|
sha256 = "16vpicp4q2kbpgr3qwpsxg7srabxqszx23x6smjvvrvz7qmr5v8i";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NETStandard.Library";
|
||||||
|
version = "2.0.0";
|
||||||
|
sha256 = "1bc4ba8ahgk15m8k4nd7x406nhi0kwqzbgjk2dmw52ss553xz7iy";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "Newtonsoft.Json";
|
||||||
|
version = "13.0.1";
|
||||||
|
sha256 = "0fijg0w6iwap8gvzyjnndds0q4b8anwxxvik7y8vgq97dram4srb";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NuGet.Common";
|
||||||
|
version = "6.6.1";
|
||||||
|
sha256 = "1q7k5rqwchxgs5pnrn22d1rkdb7l2qblvsb9hy046ll69i71vv45";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NuGet.Configuration";
|
||||||
|
version = "6.6.1";
|
||||||
|
sha256 = "0pw4ikd8784iya920wxigacqn5g2v0zlpwxjlswyq5mnj2ha7gpk";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NuGet.Frameworks";
|
||||||
|
version = "5.11.0";
|
||||||
|
sha256 = "0wv26gq39hfqw9md32amr5771s73f5zn1z9vs4y77cgynxr73s4z";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NuGet.Frameworks";
|
||||||
|
version = "6.6.1";
|
||||||
|
sha256 = "1zq79mklzq7qyiyhcv3w8pznw6rq1ddcl8fvy7j1c6n8qh3mglhx";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NuGet.Packaging";
|
||||||
|
version = "6.6.1";
|
||||||
|
sha256 = "1lmx8kgpg220q8kic4wm8skccj53cbkdqggirq9js34gnxxi9b88";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NuGet.Protocol";
|
||||||
|
version = "6.6.1";
|
||||||
|
sha256 = "01n8cw114npvzfk3m3803lb8plk0wm1zg496gpq9az8hw20nmd8g";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NuGet.Versioning";
|
||||||
|
version = "6.6.1";
|
||||||
|
sha256 = "0n2p05y8ciw6jc5s238rlnx6q4dgxvm14v06pcd84ji5j1iirc30";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NUnit";
|
||||||
|
version = "3.13.3";
|
||||||
|
sha256 = "0wdzfkygqnr73s6lpxg5b1pwaqz9f414fxpvpdmf72bvh4jaqzv6";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NUnit.Analyzers";
|
||||||
|
version = "3.6.1";
|
||||||
|
sha256 = "16dw5375k2wyhiw9x387y7pjgq6zms30y036qb8z7idx4lxw9yi9";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "NUnit3TestAdapter";
|
||||||
|
version = "4.4.2";
|
||||||
|
sha256 = "1n2jlc16vjdd81cb1by4qbp75sq73zsjz5w3zc61ssmbdci1q2ri";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "RestEase";
|
||||||
|
version = "1.6.4";
|
||||||
|
sha256 = "1mvi3nbrr450g3fgd1y4wg3bwl9k1agyjfd9wdkqk12714bsln8l";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Formats.Asn1";
|
||||||
|
version = "5.0.0";
|
||||||
|
sha256 = "1axc8z0839yvqi2cb63l73l6d9j6wd20lsbdymwddz9hvrsgfwpn";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.IO.Abstractions";
|
||||||
|
version = "4.2.13";
|
||||||
|
sha256 = "0s784iphsmj4vhkrzq9q3w39vsn76w44zclx3hsygsw458zbyh4y";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.IO.FileSystem.AccessControl";
|
||||||
|
version = "4.5.0";
|
||||||
|
sha256 = "1gq4s8w7ds1sp8f9wqzf8nrzal40q5cd2w4pkf4fscrl2ih3hkkj";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Reflection.Metadata";
|
||||||
|
version = "1.6.0";
|
||||||
|
sha256 = "1wdbavrrkajy7qbdblpbpbalbdl48q3h34cchz24gvdgyrlf15r4";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Runtime.CompilerServices.Unsafe";
|
||||||
|
version = "6.0.0";
|
||||||
|
sha256 = "0qm741kh4rh57wky16sq4m0v05fxmkjjr87krycf5vp9f0zbahbc";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Security.AccessControl";
|
||||||
|
version = "4.5.0";
|
||||||
|
sha256 = "1wvwanz33fzzbnd2jalar0p0z3x0ba53vzx1kazlskp7pwyhlnq0";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Security.Cryptography.Cng";
|
||||||
|
version = "5.0.0";
|
||||||
|
sha256 = "06hkx2za8jifpslkh491dfwzm5dxrsyxzj5lsc0achb6yzg4zqlw";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Security.Cryptography.Pkcs";
|
||||||
|
version = "5.0.0";
|
||||||
|
sha256 = "0hb2mndac3xrw3786bsjxjfh19bwnr991qib54k6wsqjhjyyvbwj";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Security.Cryptography.ProtectedData";
|
||||||
|
version = "4.4.0";
|
||||||
|
sha256 = "1q8ljvqhasyynp94a1d7jknk946m20lkwy2c3wa8zw2pc517fbj6";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Security.Principal.Windows";
|
||||||
|
version = "4.5.0";
|
||||||
|
sha256 = "0rmj89wsl5yzwh0kqjgx45vzf694v9p92r4x4q6yxldk1cv1hi86";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Text.Encodings.Web";
|
||||||
|
version = "6.0.0";
|
||||||
|
sha256 = "06n9ql3fmhpjl32g3492sj181zjml5dlcc5l76xq2h38c4f87sai";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Text.Encodings.Web";
|
||||||
|
version = "7.0.0";
|
||||||
|
sha256 = "1151hbyrcf8kyg1jz8k9awpbic98lwz9x129rg7zk1wrs6vjlpxl";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Text.Json";
|
||||||
|
version = "6.0.0";
|
||||||
|
sha256 = "1si2my1g0q0qv1hiqnji4xh9wd05qavxnzj9dwgs23iqvgjky0gl";
|
||||||
|
})
|
||||||
|
(fetchNuGet {
|
||||||
|
pname = "System.Text.Json";
|
||||||
|
version = "7.0.3";
|
||||||
|
sha256 = "0zjrnc9lshagm6kdb9bdh45dmlnkpwcpyssa896sda93ngbmj8k9";
|
||||||
|
})
|
||||||
|
]
|
73
nix/fetchDeps.sh
Executable file
73
nix/fetchDeps.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This file was adapted from
|
||||||
|
# https://github.com/NixOS/nixpkgs/blob/b981d811453ab84fb3ea593a9b33b960f1ab9147/pkgs/build-support/dotnet/build-dotnet-module/default.nix#L173
|
||||||
|
set -euo pipefail
|
||||||
|
export PATH="@binPath@"
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--keep-sources|-k)
|
||||||
|
keepSources=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
echo "usage: $0 [--keep-sources] [--help] <output path>"
|
||||||
|
echo " <output path> The path to write the lockfile to. A temporary file is used if this is not set"
|
||||||
|
echo " --keep-sources Don't remove temporary directories upon exit, useful for debugging"
|
||||||
|
echo " --help Show this help message"
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
tmp=$(mktemp -td "@pname@-tmp-XXXXXX")
|
||||||
|
export tmp
|
||||||
|
HOME=$tmp/home
|
||||||
|
exitTrap() {
|
||||||
|
test -n "${ranTrap-}" && return
|
||||||
|
ranTrap=1
|
||||||
|
if test -n "${keepSources-}"; then
|
||||||
|
echo -e "Path to the source: $tmp/src\nPath to the fake home: $tmp/home"
|
||||||
|
else
|
||||||
|
rm -rf "$tmp"
|
||||||
|
fi
|
||||||
|
# Since mktemp is used this will be empty if the script didnt succesfully complete
|
||||||
|
if ! test -s "$depsFile"; then
|
||||||
|
rm -rf "$depsFile"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap exitTrap EXIT INT TERM
|
||||||
|
dotnetRestore() {
|
||||||
|
local -r project="${1-}"
|
||||||
|
local -r rid="$2"
|
||||||
|
dotnet restore "${project-}" \
|
||||||
|
-p:ContinuousIntegrationBuild=true \
|
||||||
|
-p:Deterministic=true \
|
||||||
|
--packages "$tmp/nuget_pkgs" \
|
||||||
|
--runtime "$rid" \
|
||||||
|
--no-cache \
|
||||||
|
--force
|
||||||
|
}
|
||||||
|
declare -a projectFiles=( @projectFiles@ )
|
||||||
|
declare -a testProjectFiles=( @testProjectFiles@ )
|
||||||
|
export DOTNET_NOLOGO=1
|
||||||
|
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
depsFile=$(realpath "${1:-$(mktemp -t "@pname@-deps-XXXXXX.nix")}")
|
||||||
|
mkdir -p "$tmp/nuget_pkgs"
|
||||||
|
storeSrc="@storeSrc@"
|
||||||
|
src="$tmp/src"
|
||||||
|
cp -rT "$storeSrc" "$src"
|
||||||
|
chmod -R +w "$src"
|
||||||
|
cd "$src"
|
||||||
|
echo "Restoring project..."
|
||||||
|
rids=("@rids@")
|
||||||
|
for rid in "${rids[@]}"; do
|
||||||
|
(( ${#projectFiles[@]} == 0 )) && dotnetRestore "" "$rid"
|
||||||
|
for project in "${projectFiles[@]-}" "${testProjectFiles[@]-}"; do
|
||||||
|
dotnetRestore "$project" "$rid"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
echo "Successfully restored project"
|
||||||
|
echo "Writing lockfile..."
|
||||||
|
echo -e "# This file was automatically generated by passthru.fetch-deps.\n# Please don't edit it manually, your changes might get overwritten!\n" > "$depsFile"
|
||||||
|
nuget-to-nix "$tmp/nuget_pkgs" "@packages@" >> "$depsFile"
|
||||||
|
echo "Successfully wrote lockfile to $depsFile"
|
Reference in New Issue
Block a user