From f77c2932ecdee15a4c8a65ceb5b2ead5ee4ee0a1 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Fri, 17 Jun 2016 13:37:49 -0500 Subject: [PATCH] All OpenStates Methods Implemented --- example/example.go | 8 ++- openstates/bill_structs.go | 5 -- openstates/bills.go | 94 ++++++++++++++++++++++----------- openstates/committee_structs.go | 28 ++++++++++ openstates/committees.go | 73 +++++++++++++++++++++++++ openstates/district_structs.go | 46 ++++++++++++++++ openstates/districts.go | 56 ++++++++++++++++++++ openstates/event_structs.go | 54 +++++++++++++++++++ openstates/events.go | 70 ++++++++++++++++++++++++ openstates/legislators.go | 4 +- openstates/openstates.go | 14 +++++ openstates/os_structs.go | 42 ++++++++------- 12 files changed, 437 insertions(+), 57 deletions(-) create mode 100644 openstates/committee_structs.go create mode 100644 openstates/committees.go create mode 100644 openstates/district_structs.go create mode 100644 openstates/districts.go create mode 100644 openstates/event_structs.go create mode 100644 openstates/events.go diff --git a/example/example.go b/example/example.go index 8614b5a..48d6249 100644 --- a/example/example.go +++ b/example/example.go @@ -21,7 +21,13 @@ func main() { //d, err := o.GetBillDetail(states.Kansas, "2013-2014", "HR 6020") //d, err := o.GetLegislatorsForState(states.Kansas) //d, err := o.GetLegislatorDetail("KSL000018") - d, err := o.GetLegislatorsForGeo(35.79, -78.78) + //d, err := o.GetLegislatorsForGeo(35.79, -78.78) + //v := url.Values{} + //d, err := o.SearchEvents(v) + //d, err := o.GetEventsForState(states.Texas) + //d, err := o.GetEventDetail("TXE00028743") + //d, err := o.GetDistrictsForState(states.Kansas) + d, err := o.GetDistrictBoundary("ocd-division/country:us/state:ks/sldu:33") if err != nil { fmt.Println(err.Error()) return diff --git a/openstates/bill_structs.go b/openstates/bill_structs.go index 58759fe..3bd6788 100644 --- a/openstates/bill_structs.go +++ b/openstates/bill_structs.go @@ -52,11 +52,6 @@ type Action struct { Actor string `json:"actor"` } -// Source is a source in a bill -type Source struct { - URL string `json:"url"` -} - // Sponsor is a sponsor on a bill type Sponsor struct { LegID string `json:"leg_id"` diff --git a/openstates/bills.go b/openstates/bills.go index d78879b..ba12e2b 100644 --- a/openstates/bills.go +++ b/openstates/bills.go @@ -2,16 +2,49 @@ package openstates import ( "encoding/json" + "errors" "fmt" "net/url" - "strconv" - "time" "github.com/br0xen/sunlight-api/openstates/states" ) -// GetBillsWithParams returns all bills with parameters 'v' -func (o *OpenStates) GetBillsWithParams(v url.Values) ([]Bill, error) { +// SearchBills returns all bills with parameters 'v' +// Valid Parameters: +// * state - Filter by state +// * chamber - Filter by chamber +// * bill_id - Only bills with id bill_id +// * q - Bills matching provided full text query +// * search_window - By default all bills are searched, but you +// can limit the search window with these: +// * all - Default, include all sessions +// * term - Only bills from the current term +// * session - Only bills from the current session +// * session:2009 - Only bills from the session named 2009 +// * term:2009-2011 - Only bills from the term named 2009-2011 +// * updated_since - Only bills updated since the provided date +// * sponsor_id - Bills sponsored by a given legislator id +// * subject - Only bills categorized by OpenStates as +// belonging to this subject +// * type - Only bills of a given type +// ('bill', 'resolution', etc.) +// Additionally, 'sort' is a valid parameter, defaults to 'last' +// Other sort options: +// * first - Oldest first +// * last - Newest first (default) +// * signed - Signed first +// * passed_lower - Passed Lower first +// * passed_upper - Passed Upper first +// * updated_at - Sort by updated_at time +// * created_at - Sort by created_at time +// +// The API will not return exceedingly large responses, so it may +// be necessary to use 'page' and 'per_page' to control the # of +// of results returned: +// page - Page of results, each of size per_page (defaults to 1) +// per_page - Number of results per page, is unlimited unless page is +// set, in which case it defaults to 50. +func (o *OpenStates) SearchBills(v url.Values) ([]Bill, error) { var ret []Bill var err error var getVal []byte @@ -21,14 +54,7 @@ func (o *OpenStates) GetBillsWithParams(v url.Values) ([]Bill, error) { err = json.Unmarshal(getVal, &ret) if err == nil { for i := range ret { - ret[i].CreatedAt, err = time.Parse("2006-01-02 15:04:05", ret[i].CreatedAtStr) - if err != nil { - fmt.Println("Error on idx: " + strconv.Itoa(i)) - return ret, err - } - ret[i].UpdatedAt, err = time.Parse("2006-01-02 15:04:05", ret[i].UpdatedAtStr) - if err != nil { - fmt.Println("Error on idx: " + strconv.Itoa(i)) + if err = o.fixBillTimes(&ret[i]); err != nil { return ret, err } } @@ -44,7 +70,7 @@ func (o *OpenStates) GetBillsForState(st string) ([]Bill, error) { } vals := url.Values{} vals.Set("state", st) - return o.GetBillsWithParams(vals) + return o.SearchBills(vals) } // QueryBillsForState Does a search for bills with text 'q' in state 'st @@ -56,7 +82,7 @@ func (o *OpenStates) QueryBillsForState(st string, q string) ([]Bill, error) { vals := url.Values{} vals.Set("state", st) vals.Set("q", q) - return o.GetBillsWithParams(vals) + return o.SearchBills(vals) } func (o *OpenStates) getBillDetailForEndpoint(ep string) (*Bill, error) { @@ -71,23 +97,7 @@ func (o *OpenStates) getBillDetailForEndpoint(ep string) (*Bill, error) { err = json.Unmarshal(getVal, &ret) ret.HasDetail = true if err == nil { - if err = UnmarshalTimeString(ret.CreatedAtStr, &ret.CreatedAt); err != nil { - return ret, err - } - if err = UnmarshalTimeString(ret.UpdatedAtStr, &ret.UpdatedAt); err != nil { - return ret, err - } - UnmarshalTimeString(ret.ActionDates.PassedUpperStr, &ret.ActionDates.PassedUpper) - UnmarshalTimeString(ret.ActionDates.PassedLowerStr, &ret.ActionDates.PassedLower) - UnmarshalTimeString(ret.ActionDates.LastStr, &ret.ActionDates.Last) - UnmarshalTimeString(ret.ActionDates.SignedStr, &ret.ActionDates.Signed) - UnmarshalTimeString(ret.ActionDates.FirstStr, &ret.ActionDates.First) - for i := range ret.Actions { - UnmarshalTimeString(ret.Actions[i].DateStr, &ret.Actions[i].Date) - } - for i := range ret.Votes { - UnmarshalTimeString(ret.Votes[i].DateStr, &ret.Votes[i].Date) - } + err = o.fixBillTimes(ret) } return ret, err } @@ -105,3 +115,25 @@ func (o *OpenStates) GetBillDetail(st, sess, name string) (*Bill, error) { func (o *OpenStates) GetBillDetailFromID(id string) (*Bill, error) { return o.getBillDetailForEndpoint("bills/" + id) } + +func (o *OpenStates) fixBillTimes(b *Bill) error { + var err error + if err = UnmarshalTimeString(b.CreatedAtStr, &b.CreatedAt); err != nil { + return errors.New("No Created At Time") + } + if err = UnmarshalTimeString(b.UpdatedAtStr, &b.UpdatedAt); err != nil { + return errors.New("No Updated At Time") + } + UnmarshalTimeString(b.ActionDates.PassedUpperStr, &b.ActionDates.PassedUpper) + UnmarshalTimeString(b.ActionDates.PassedLowerStr, &b.ActionDates.PassedLower) + UnmarshalTimeString(b.ActionDates.LastStr, &b.ActionDates.Last) + UnmarshalTimeString(b.ActionDates.SignedStr, &b.ActionDates.Signed) + UnmarshalTimeString(b.ActionDates.FirstStr, &b.ActionDates.First) + for i := range b.Actions { + UnmarshalTimeString(b.Actions[i].DateStr, &b.Actions[i].Date) + } + for i := range b.Votes { + UnmarshalTimeString(b.Votes[i].DateStr, &b.Votes[i].Date) + } + return nil +} diff --git a/openstates/committee_structs.go b/openstates/committee_structs.go new file mode 100644 index 0000000..358b17a --- /dev/null +++ b/openstates/committee_structs.go @@ -0,0 +1,28 @@ +package openstates + +import "time" + +// Committee is a committee +type Committee struct { + Level string `json:"level"` + CreatedAtStr string `json:"created_at"` + CreatedAt time.Time `json:"-"` + UpdatedAtStr string `json:"updated_at"` + UpdatedAt time.Time `json:"-"` + ParentID string `json:"parent_id"` + State string `json:"state"` + Subcommittee string `json:"subcommittee"` + Name string `json:"committee"` + Chamber string `json:"chamber"` + ID string `json:"id"` + HasDetail bool `json:"-"` + Members []CommitteeMember `json:"members"` + Sources []Source `json:"sources"` +} + +// CommitteeMember is a member of a committee +type CommitteeMember struct { + LegID string `json:"leg_id"` + Role string `json:"role"` + Name string `json:"name"` +} diff --git a/openstates/committees.go b/openstates/committees.go new file mode 100644 index 0000000..13d3ae6 --- /dev/null +++ b/openstates/committees.go @@ -0,0 +1,73 @@ +package openstates + +import ( + "encoding/json" + "errors" + "net/url" + + "github.com/br0xen/sunlight-api/openstates/states" +) + +// SearchCommittees retrieves a list of committees based on a number of +// parameters. +// Valid Parameters: +// * committee +// * subcommittee +// * chamber +// * state +func (o *OpenStates) SearchCommittees(v url.Values) ([]Committee, error) { + var ret []Committee + var err error + var getVal []byte + if getVal, err = o.call("committees/", v); err != nil { + return ret, err + } + err = json.Unmarshal(getVal, &ret) + if err == nil { + for i := range ret { + if err = o.fixCommitteeTimes(&ret[i]); err != nil { + return ret, err + } + } + } + return ret, err +} + +// GetCommitteesForState gets all committees for a state +func (o *OpenStates) GetCommitteesForState(st string) ([]Committee, error) { + st, err := states.ScrubToAbbr(st) + if err != nil { + return []Committee{}, err + } + v := url.Values{} + v.Set("state", st) + return o.SearchCommittees(v) +} + +// GetCommitteeDetail gets the details for a committee +func (o *OpenStates) GetCommitteeDetail(id string) (*Committee, error) { + var ret *Committee + var err error + var getVal []byte + v := url.Values{} + if getVal, err = o.call("committees/"+id+"/", v); err != nil { + return ret, err + } + err = json.Unmarshal(getVal, &ret) + if err == nil { + if err = o.fixCommitteeTimes(ret); err != nil { + return ret, err + } + } + return ret, err +} + +func (o *OpenStates) fixCommitteeTimes(c *Committee) error { + if err := UnmarshalTimeString(c.CreatedAtStr, &c.CreatedAt); err != nil { + return errors.New("No Created At Time") + } + if err := UnmarshalTimeString(c.UpdatedAtStr, &c.UpdatedAt); err != nil { + return errors.New("No Updated At Time") + } + return nil +} diff --git a/openstates/district_structs.go b/openstates/district_structs.go new file mode 100644 index 0000000..2e70708 --- /dev/null +++ b/openstates/district_structs.go @@ -0,0 +1,46 @@ +package openstates + +// District is a district +type District struct { + Abbr string `json:"abbr"` + BoundaryID string `json:"boundary_id"` + Chamber string `json:"chamber"` + ID string `json:"id"` + Legislators []DistrictLegislator `json:"legislators"` + Name string `json:"name"` + NumSeats int `json:"num_seats"` +} + +// DistrictLegislator is a legislator entry in a district +type DistrictLegislator struct { + FullName string `json:"full_name"` + LegID string `json:"leg_id"` +} + +// DistrictBoundary describes the boundary of a district +type DistrictBoundary struct { + DivisionID string `json:"division_id"` + Name string `json:"name"` + Region *DistrictRegion `json:"region"` + Chamber string `json:"chamber"` + Shape *DistrictShape `json:"shape"` + Abbr string `json:"abbr"` + BoundaryID string `json:"boundary_id"` + NumSeats int `json:"num_seats"` + ID string `json:"id"` + BBox [2]DistrictBoundaryXY `json:"bbox"` +} + +// DistrictBoundaryXY is an x/y +type DistrictBoundaryXY [2]float64 + +// DistrictRegion is a bounding region +type DistrictRegion struct { + CenterLat float64 `json:"center_lat"` + CenterLon float64 `json:"center_lon"` + LatDelta float64 `json:"lat_delta"` + LonDelta float64 `json:"lon_delta"` +} + +// DistrictShape is a bounding shape +type DistrictShape [][][]DistrictBoundaryXY diff --git a/openstates/districts.go b/openstates/districts.go new file mode 100644 index 0000000..efe9571 --- /dev/null +++ b/openstates/districts.go @@ -0,0 +1,56 @@ +package openstates + +import ( + "encoding/json" + "errors" + "net/url" + + "github.com/br0xen/sunlight-api/openstates/states" +) + +// SearchDistricts retrieves a list of districts based on a number of +// parameters +// Valid +func (o *OpenStates) SearchDistricts(v url.Values) ([]District, error) { + var ret []District + var err error + if v.Get("state") == "" { + return ret, errors.New("SearchDistricts: State is required") + } + var getVal []byte + ep := "districts/" + v.Get("state") + "/" + if v.Get("chamber") != "" { + ep += v.Get("chamber") + "/" + v.Del("chamber") + } + v.Del("state") + if getVal, err = o.call(ep, v); err != nil { + return ret, err + } + err = json.Unmarshal(getVal, &ret) + return ret, err +} + +// GetDistrictsForState returns all districts for state st +func (o *OpenStates) GetDistrictsForState(st string) ([]District, error) { + st, err := states.ScrubToAbbr(st) + if err != nil { + return []District{}, err + } + vals := url.Values{} + vals.Set("state", st) + return o.SearchDistricts(vals) +} + +// GetDistrictBoundary returns the boundary object for a district +func (o *OpenStates) GetDistrictBoundary(bid string) (*DistrictBoundary, error) { + var ret *DistrictBoundary + var err error + var getVal []byte + v := url.Values{} + if getVal, err = o.call("/districts/boundary/"+bid, v); err != nil { + return ret, err + } + err = json.Unmarshal(getVal, &ret) + return ret, err +} diff --git a/openstates/event_structs.go b/openstates/event_structs.go new file mode 100644 index 0000000..924eb7a --- /dev/null +++ b/openstates/event_structs.go @@ -0,0 +1,54 @@ +package openstates + +import "time" + +// Event is an event +type Event struct { + Documents []Document `json:"documents"` + End time.Time `json:"-"` + EndStr string `json:"end"` + Description string `json:"description"` + State string `json:"state"` + Agenda string `json:"+agenda"` + CreatedAtStr string `json:"created_at"` + CreatedAt time.Time `json:"-"` + When time.Time `json:"-"` + WhenStr string `json:"when"` + UpdatedAtStr string `json:"updated_at"` + UpdatedAt time.Time `json:"-"` + Sources []Source `json:"sources"` + Participants []Participant `json:"participants"` + Session string `json:"session"` + Location string `json:"location"` + RelatedBills []EventBill `json:"related_bills"` + Timezone string `json:"timezone"` + Type string `json:"type"` + ID string `json:"id"` + Chamber string `json:"+chamber"` +} + +// EventBill is a bill associated with an event +type EventBill struct { + Description string `json:"description"` + Type string `json:"type"` + ID string `json:"id"` + BillID string `json:"bill_id"` + Chamber string `json:"+chamber"` +} + +// Document is a document for an event (agenda, etc) +type Document struct { + URL string `json:"url"` + MimeType string `json:"+mimetype"` + Name string `json:"name"` + Type string `json:"+type"` +} + +// Participant is a participant in an event +type Participant struct { + Chamber string `json:"chamber"` + ParticipantType string `json:"participant_type"` + Name string `json:"participant"` + ID string `json:"id"` + Type string `json:"type"` +} diff --git a/openstates/events.go b/openstates/events.go new file mode 100644 index 0000000..2ed3447 --- /dev/null +++ b/openstates/events.go @@ -0,0 +1,70 @@ +package openstates + +import ( + "encoding/json" + "errors" + "net/url" + + "github.com/br0xen/sunlight-api/openstates/states" +) + +// SearchEvents retrieves a list of events based on some parameters +// Valid Parameters +// * state - Filter by state +// * type - Filter by type +func (o *OpenStates) SearchEvents(v url.Values) ([]Event, error) { + var ret []Event + var err error + var getVal []byte + if getVal, err = o.call("events", v); err != nil { + return ret, err + } + err = json.Unmarshal(getVal, &ret) + if err == nil { + for i := range ret { + if err = o.fixEventTimes(&ret[i]); err != nil { + return ret, err + } + } + } + return ret, err +} + +// GetEventsForState returns all events for a state +func (o *OpenStates) GetEventsForState(st string) ([]Event, error) { + st, err := states.ScrubToAbbr(st) + if err != nil { + return []Event{}, err + } + v := url.Values{} + v.Set("state", st) + return o.SearchEvents(v) +} + +// GetEventDetail returns event detail for an event id +func (o *OpenStates) GetEventDetail(id string) (*Event, error) { + var ret *Event + var err error + var getVal []byte + v := url.Values{} + if getVal, err = o.call("events/"+id+"/", v); err != nil { + return ret, err + } + err = json.Unmarshal(getVal, &ret) + if err == nil { + err = o.fixEventTimes(ret) + } + return ret, err +} + +func (o *OpenStates) fixEventTimes(e *Event) error { + if err := UnmarshalTimeString(e.CreatedAtStr, &e.CreatedAt); err != nil { + return errors.New("No Created At Time") + } + if err := UnmarshalTimeString(e.UpdatedAtStr, &e.UpdatedAt); err != nil { + return errors.New("No Updated At Time") + } + UnmarshalTimeString(e.EndStr, &e.End) + UnmarshalTimeString(e.WhenStr, &e.When) + return nil +} diff --git a/openstates/legislators.go b/openstates/legislators.go index 370ef01..1723e7d 100644 --- a/openstates/legislators.go +++ b/openstates/legislators.go @@ -32,7 +32,9 @@ func (o *OpenStates) SearchLegislators(v url.Values) ([]Legislator, error) { err = json.Unmarshal(getVal, &ret) if err == nil { for i := range ret { - o.fixLegislatorTimes(&ret[i]) + if err = o.fixLegislatorTimes(&ret[i]); err != nil { + return ret, err + } } } return ret, err diff --git a/openstates/openstates.go b/openstates/openstates.go index 905fbe1..7196983 100644 --- a/openstates/openstates.go +++ b/openstates/openstates.go @@ -70,6 +70,20 @@ func (o *OpenStates) StateMetadata(st string) (*StateMeta, error) { return ret, err } +func (o *OpenStates) fixStateMetaTimes(m *StateMeta) error { + var err error + if err = UnmarshalTimeString(m.LatestCSVDateStr, &m.LatestCSVDate); err != nil { + return errors.New("No CSV Date") + } + if err = UnmarshalTimeString(m.LatestJSONDateStr, &m.LatestJSONDate); err != nil { + return errors.New("No JSON Date") + } + if err = UnmarshalTimeString(m.LatestUpdateStr, &m.LatestUpdate); err != nil { + return errors.New("No Latest Update Time") + } + return err +} + // UnmarshalTimeString Takes a time string and a pointer to a time object // and populates the time object with the value from the string func UnmarshalTimeString(s string, t *time.Time) error { diff --git a/openstates/os_structs.go b/openstates/os_structs.go index f17c70b..536abae 100644 --- a/openstates/os_structs.go +++ b/openstates/os_structs.go @@ -10,25 +10,24 @@ type OpenStates struct { // StateMeta is all of the metadata for a state type StateMeta struct { - Name string `json:"name"` - Abbr string `json:"abbreviation"` - Chambers map[string]StateChamber `json:"chambers"` - FeatureFlags []string `json:"feature_flags"` - CapitolTimezone time.Location `json:"-"` - CapitolTimezoneStr string `json:"capitol_timezone"` - ID string `json:"id"` - LatestCSVDate time.Time `json:"-"` - LatestCSVDateStr string `json:"latest_csv_date"` - LatestCSVURL string `json:"latest_csv_url"` - LatestJSONDate time.Time `json:"-"` - LatestJSONDateStr string `json:"latest_json_date"` - LatestJSONURL string `json:"latest_json_url"` - LatestUpdate time.Time `json:"-"` - LatestUpdateStr string `json:"latest_update"` - LegislatureName string `json:"legislature_name"` - LegislatureURL string `json:"legislature_url"` - SessionDetails map[string]SessionDetail `json:"session_details"` - Terms []Term `json:"terms"` + Name string `json:"name"` + Abbr string `json:"abbreviation"` + Chambers map[string]StateChamber `json:"chambers"` + FeatureFlags []string `json:"feature_flags"` + CapitolTimezone string `json:"capitol_timezone"` + ID string `json:"id"` + LatestCSVDate time.Time `json:"-"` + LatestCSVDateStr string `json:"latest_csv_date"` + LatestCSVURL string `json:"latest_csv_url"` + LatestJSONDate time.Time `json:"-"` + LatestJSONDateStr string `json:"latest_json_date"` + LatestJSONURL string `json:"latest_json_url"` + LatestUpdate time.Time `json:"-"` + LatestUpdateStr string `json:"latest_update"` + LegislatureName string `json:"legislature_name"` + LegislatureURL string `json:"legislature_url"` + SessionDetails map[string]SessionDetail `json:"session_details"` + Terms []Term `json:"terms"` } // StateChamber is a Chamber in the state's government @@ -52,3 +51,8 @@ type Term struct { Name string `json:"name"` Sessions []string `json:"sessions"` } + +// Source is a source in a bill/committee, etc. +type Source struct { + URL string `json:"url"` +}