mirror of
https://github.com/GAM-team/GAM.git
synced 2025-07-08 21:53:36 +00:00
huge dump for Classroom, CloudPrint and batch fixes/improvements
This commit is contained in:
@ -10,6 +10,7 @@ xcopy LICENSE gam\
|
||||
xcopy whatsnew.txt gam\
|
||||
xcopy cacert.pem gam\
|
||||
xcopy admin-settings-v1.json gam\
|
||||
xcopy cloudprint-v2.json gam\
|
||||
del gam\w9xpopen.exe
|
||||
"%ProgramFiles(x86)%\7-Zip\7z.exe" a -tzip gam-%1-windows.zip gam\ -xr!.svn
|
||||
|
||||
@ -18,4 +19,5 @@ xcopy LICENSE gam-64\
|
||||
xcopy whatsnew.txt gam-64\
|
||||
xcopy cacert.pem gam-64\
|
||||
xcopy admin-settings-v1.json gam-64\
|
||||
xcopy cloudprint-v2.json gam-64\
|
||||
"%ProgramFiles(x86)%\7-Zip\7z.exe" a -tzip gam-%1-windows-x64.zip gam-64\ -xr!.svn
|
||||
|
486
cloudprint-v2.json
Normal file
486
cloudprint-v2.json
Normal file
@ -0,0 +1,486 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "cloudprint:v2",
|
||||
"name": "cloudprint",
|
||||
"version": "v2",
|
||||
"revision": "20150605",
|
||||
"title": "Cloud Print API",
|
||||
"description": "Lets you access Cloud Print Printers",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"documentationLink": "https://developers.google.com/cloud-print",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://www.google.com/",
|
||||
"basePath": "/cloudprint/",
|
||||
"rootUrl": "https://www.google.com/",
|
||||
"servicePath": "/cloudprint/",
|
||||
"parameters": {
|
||||
"prettyPrint": {
|
||||
"type": "boolean",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"default": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://www.googleapis.com/auth/cloudprint": {
|
||||
"description": "Manage Cloud Print"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Job": {
|
||||
"id": "Job",
|
||||
"type": "object",
|
||||
"description": "Job Object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Job Title"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jobs": {
|
||||
"id": "Jobs",
|
||||
"type": "object",
|
||||
"description": "List of Jobs.",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "array",
|
||||
"description": "List of job objects.",
|
||||
"items": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Printer": {
|
||||
"id": "Printer",
|
||||
"type": "object",
|
||||
"description": "Printer Object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Display Name"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Printers": {
|
||||
"id": "Printers",
|
||||
"type": "object",
|
||||
"description": "List of Printers.",
|
||||
"properties": {
|
||||
"printers": {
|
||||
"type": "array",
|
||||
"description": "List of printer objects.",
|
||||
"items": {
|
||||
"$ref": "Printer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"jobs": {
|
||||
"methods": {
|
||||
"delete": {
|
||||
"id": "cloudprint.jobs.delete",
|
||||
"path": "deletejob",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"required": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fetch": {
|
||||
"id": "cloudprint.jobs.fetch",
|
||||
"path": "fetch",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"getticket": {
|
||||
"id": "cloudprint.jobs.getticket",
|
||||
"path": "ticket",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"use_cjt": {
|
||||
"type": "boolean",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"id": "cloudprint.jobs.list",
|
||||
"path": "jobs",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"q": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"offset": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"limit": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"sortorder": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"id": "cloudprint.jobs.update",
|
||||
"path": "control",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"semantic_state_diff": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"resubmit": {
|
||||
"id": "cloudprint.jobs.resubmit",
|
||||
"path": "resubmit",
|
||||
"httpMethod": "POST",
|
||||
"description": "resubmit a job to new printer.",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"id": "cloudprint.jobs.submit",
|
||||
"path": "submit",
|
||||
"httpMethod": "POST",
|
||||
"description": "Send a print job to cloud print.",
|
||||
"request": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"contentType": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"printers": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "cloudprint.printers.get",
|
||||
"path": "printer",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"extra_fields": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Printer"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"id": "cloudprint.printers.list",
|
||||
"path": "search",
|
||||
"httpMethod": "GET",
|
||||
"description": "List all printers",
|
||||
"parameters": {
|
||||
"q": {
|
||||
"type": "string",
|
||||
"description": "Query list of printers",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "limit results to printers of type",
|
||||
"location": "query"
|
||||
},
|
||||
"connection_status": {
|
||||
"type": "string",
|
||||
"description": "limit results to printers with this status",
|
||||
"location": "query"
|
||||
},
|
||||
"extra_fields": {
|
||||
"type": "string",
|
||||
"description": "include extra fields",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Printers"
|
||||
}
|
||||
},
|
||||
"share": {
|
||||
"id": "cloudprint.printers.share",
|
||||
"path": "share",
|
||||
"httpMethod": "GET",
|
||||
"description": "Share printer with user, group or domain",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"skip_notification": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unshare": {
|
||||
"id": "cloudprint.printers.unshare",
|
||||
"path": "unshare",
|
||||
"httpMethod": "GET",
|
||||
"description": "unshare printer with user, group or domain",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"id": "cloudprint.printers.delete",
|
||||
"path": "delete",
|
||||
"httpMethod": "GET",
|
||||
"description": "delete a printer",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"id": "cloudprint.printers.update",
|
||||
"path": "update",
|
||||
"httpMethod": "GET",
|
||||
"description": "update a printer",
|
||||
"parameters": {
|
||||
"isTosAccepted": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"gcpVersion": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"setupUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"supportUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"firmware": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"currentQuota": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"proxy": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"defaultDisplayName": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"updateUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"quotaEnabled": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"dailyQuota": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -12,4 +12,4 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__version__ = "1.4.0"
|
||||
__version__ = "1.4.1"
|
||||
|
@ -56,6 +56,7 @@ from googleapiclient.errors import MediaUploadSizeError
|
||||
from googleapiclient.errors import UnacceptableMimeTypeError
|
||||
from googleapiclient.errors import UnknownApiNameOrVersion
|
||||
from googleapiclient.errors import UnknownFileType
|
||||
from googleapiclient.http import BatchHttpRequest
|
||||
from googleapiclient.http import HttpRequest
|
||||
from googleapiclient.http import MediaFileUpload
|
||||
from googleapiclient.http import MediaUpload
|
||||
@ -950,6 +951,27 @@ class Resource(object):
|
||||
self._add_next_methods(self._resourceDesc, self._schema)
|
||||
|
||||
def _add_basic_methods(self, resourceDesc, rootDesc, schema):
|
||||
# If this is the root Resource, add a new_batch_http_request() method.
|
||||
if resourceDesc == rootDesc:
|
||||
batch_uri = '%s%s' % (
|
||||
rootDesc['rootUrl'], rootDesc.get('batchPath', 'batch'))
|
||||
def new_batch_http_request(callback=None):
|
||||
"""Create a BatchHttpRequest object based on the discovery document.
|
||||
|
||||
Args:
|
||||
callback: callable, A callback to be called for each response, of the
|
||||
form callback(id, response, exception). The first parameter is the
|
||||
request id, and the second is the deserialized response object. The
|
||||
third is an apiclient.errors.HttpError exception object if an HTTP
|
||||
error occurred while processing the request, or None if no error
|
||||
occurred.
|
||||
|
||||
Returns:
|
||||
A BatchHttpRequest object based on the discovery document.
|
||||
"""
|
||||
return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
|
||||
self._set_dynamic_attr('new_batch_http_request', new_batch_http_request)
|
||||
|
||||
# Add basic methods to Resource
|
||||
if 'methods' in resourceDesc:
|
||||
for methodName, methodDesc in six.iteritems(resourceDesc['methods']):
|
||||
|
@ -37,6 +37,8 @@ class HttpError(Error):
|
||||
@util.positional(3)
|
||||
def __init__(self, resp, content, uri=None):
|
||||
self.resp = resp
|
||||
if not isinstance(content, bytes):
|
||||
raise TypeError("HTTP content should be bytes")
|
||||
self.content = content
|
||||
self.uri = uri
|
||||
|
||||
@ -44,7 +46,7 @@ class HttpError(Error):
|
||||
"""Calculate the reason for the error from the response content."""
|
||||
reason = self.resp.reason
|
||||
try:
|
||||
data = json.loads(self.content)
|
||||
data = json.loads(self.content.decode('utf-8'))
|
||||
reason = data['error']['message']
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
@ -1120,10 +1120,6 @@ class BatchHttpRequest(object):
|
||||
g.flatten(msg, unixfrom=False)
|
||||
body = fp.getvalue()
|
||||
|
||||
# Strip off the \n\n that the MIME lib tacks onto the end of the payload.
|
||||
if request.body is None:
|
||||
body = body[:-2]
|
||||
|
||||
return status_line + body
|
||||
|
||||
def _deserialize_response(self, payload):
|
||||
@ -1252,11 +1248,12 @@ class BatchHttpRequest(object):
|
||||
if resp.status >= 300:
|
||||
raise HttpError(resp, content, uri=self._batch_uri)
|
||||
|
||||
# Now break out the individual responses and store each one.
|
||||
boundary, _ = content.split(None, 1)
|
||||
|
||||
# Prepend with a content-type header so FeedParser can handle it.
|
||||
header = 'content-type: %s\r\n\r\n' % resp['content-type']
|
||||
# PY3's FeedParser only accepts unicode. So we should decode content
|
||||
# here, and encode each payload again.
|
||||
if six.PY3:
|
||||
content = content.decode('utf-8')
|
||||
for_parser = header + content
|
||||
|
||||
parser = FeedParser()
|
||||
@ -1270,6 +1267,9 @@ class BatchHttpRequest(object):
|
||||
for part in mime_response.get_payload():
|
||||
request_id = self._header_to_id(part['Content-ID'])
|
||||
response, content = self._deserialize_response(part.get_payload())
|
||||
# We encode content here to emulate normal http response.
|
||||
if isinstance(content, six.text_type):
|
||||
content = content.encode('utf-8')
|
||||
self._responses[request_id] = (response, content)
|
||||
|
||||
@util.positional(1)
|
||||
@ -1458,7 +1458,7 @@ class HttpMock(object):
|
||||
headers: dict, header to return with response
|
||||
"""
|
||||
if headers is None:
|
||||
headers = {'status': '200 OK'}
|
||||
headers = {'status': '200'}
|
||||
if filename:
|
||||
f = open(filename, 'r')
|
||||
self.data = f.read()
|
||||
@ -1536,6 +1536,8 @@ class HttpMockSequence(object):
|
||||
content = body
|
||||
elif content == 'echo_request_uri':
|
||||
content = uri
|
||||
if isinstance(content, six.text_type):
|
||||
content = content.encode('utf-8')
|
||||
return httplib2.Response(resp), content
|
||||
|
||||
|
||||
|
@ -22,7 +22,7 @@ __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)",
|
||||
"Sam Ruby",
|
||||
"Louis Nyffenegger"]
|
||||
__license__ = "MIT"
|
||||
__version__ = "0.9"
|
||||
__version__ = "0.9.1"
|
||||
|
||||
import re
|
||||
import sys
|
||||
@ -255,8 +255,8 @@ def safename(filename):
|
||||
filename = re_slash.sub(",", filename)
|
||||
|
||||
# limit length of filename
|
||||
if len(filename)>64:
|
||||
filename=filename[:64]
|
||||
if len(filename)>200:
|
||||
filename=filename[:200]
|
||||
return ",".join((filename, filemd5))
|
||||
|
||||
NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')
|
||||
@ -749,12 +749,27 @@ class ProxyInfo(object):
|
||||
bypass_hosts = ()
|
||||
|
||||
def __init__(self, proxy_type, proxy_host, proxy_port,
|
||||
proxy_rdns=None, proxy_user=None, proxy_pass=None):
|
||||
"""The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX
|
||||
constants. For example:
|
||||
proxy_rdns=True, proxy_user=None, proxy_pass=None):
|
||||
"""
|
||||
Args:
|
||||
proxy_type: The type of proxy server. This must be set to one of
|
||||
socks.PROXY_TYPE_XXX constants. For example:
|
||||
|
||||
p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP,
|
||||
proxy_host='localhost', proxy_port=8000)
|
||||
|
||||
proxy_host: The hostname or IP address of the proxy server.
|
||||
|
||||
proxy_port: The port that the proxy server is running on.
|
||||
|
||||
proxy_rdns: If True (default), DNS queries will not be performed
|
||||
locally, and instead, handed to the proxy to resolve. This is useful
|
||||
if the network does not allow resolution of non-local names. In
|
||||
httplib2 0.9 and earlier, this defaulted to False.
|
||||
|
||||
proxy_user: The username used to authenticate with the proxy server.
|
||||
|
||||
proxy_pass: The password used to authenticate with the proxy server.
|
||||
"""
|
||||
self.proxy_type = proxy_type
|
||||
self.proxy_host = proxy_host
|
||||
@ -871,12 +886,12 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection):
|
||||
if self.proxy_info and self.proxy_info.isgood():
|
||||
use_proxy = True
|
||||
proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple()
|
||||
else:
|
||||
use_proxy = False
|
||||
if use_proxy and proxy_rdns:
|
||||
|
||||
host = proxy_host
|
||||
port = proxy_port
|
||||
else:
|
||||
use_proxy = False
|
||||
|
||||
host = self.host
|
||||
port = self.port
|
||||
|
||||
@ -993,12 +1008,12 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
|
||||
if self.proxy_info and self.proxy_info.isgood():
|
||||
use_proxy = True
|
||||
proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple()
|
||||
else:
|
||||
use_proxy = False
|
||||
if use_proxy and proxy_rdns:
|
||||
|
||||
host = proxy_host
|
||||
port = proxy_port
|
||||
else:
|
||||
use_proxy = False
|
||||
|
||||
host = self.host
|
||||
port = self.port
|
||||
|
||||
@ -1481,7 +1496,7 @@ class Http(object):
|
||||
info = email.Message.Message()
|
||||
cached_value = None
|
||||
if self.cache:
|
||||
cachekey = defrag_uri
|
||||
cachekey = defrag_uri.encode('utf-8')
|
||||
cached_value = self.cache.get(cachekey)
|
||||
if cached_value:
|
||||
# info = email.message_from_string(cached_value)
|
||||
|
11
whatsnew.txt
11
whatsnew.txt
@ -1,3 +1,14 @@
|
||||
GAM 3.5
|
||||
-Support for the new Google Classroom API.
|
||||
-create, update, info and delete courses
|
||||
-add, remove and sync course teachers and students
|
||||
-print courses and course participants
|
||||
-Google CloudPrint API Support
|
||||
-update, info, delete and report printers
|
||||
-share, unshare and get ACLs for printers
|
||||
-submit, cancel, report and delete print jobs
|
||||
-Bug fixes and improvements to GAM batch commands
|
||||
|
||||
GAM 3.45
|
||||
-add six.py to solve compatability issues on OS X and Linux
|
||||
-be conservative with password hashing to prevent timeouts
|
||||
|
Reference in New Issue
Block a user