Git Product home page Git Product logo

p3's Introduction

P3

P3 is a modern, lean and mean PostgreSQL client for Pharo.

CI

P3Client uses frontend/backend protocol 3.0 (PostgreSQL version 7.4 [2003] and later), implementing the simple and extended query cycles. It supports plaintext, md5 and scram-sha-256 password authentication. When SQL queries return row data, it efficiently converts incoming data to objects. P3Client supports most common PostgreSQL types.

P3Client can be configured manually or through a URL.

P3Client new url: 'psql://username:password@localhost:5432/databasename'.

Not all properties need to be specified, the minimum is the following URL.

P3Client new url: 'psql://user@localhost'.

P3Client has a minimal public protocol, basically #query: (#execute: is an alias).

Opening a connection to the server (#open) and running the authentication and startup protocols (#connect) are done automatically when needed from #query.

P3Client also supports SSL connections. Use #connectSSL to initiate such a connection. Alternatively you can add sslmode=require to the connection URL, as in 'psql://username:password@localhost:5432/databasename?sslmode=require'.

Through the #prepare: message, you can ask P3Client to prepare/parse an SQL statement or query with parameters. This will give you a P3PreparedStatement instance than you can then execute with specific parameters. Polymorphic to this there is also P3FormattedStatement which you create using the #format: message. These work at the textual, client side level.

Basic Usage

Here is the simplest test that does an actual query, it should return true.

(P3Client new url: 'psql://sven@localhost') in: [ :client |
   [ client isWorking ] ensure: [ client close ] ].

This is how to create a simple table with some rows in it.

(P3Client new url: 'psql://sven@localhost') in: [ :client |
   client execute: 'DROP TABLE IF EXISTS table1'.
   client execute: 'CREATE TABLE table1 (id INTEGER, name TEXT, enabled BOOLEAN)'.
   client execute: 'INSERT INTO table1 (id, name, enabled) VALUES (1, ''foo'', true)'.
   client execute: 'INSERT INTO table1 (id, name, enabled) VALUES (2, ''bar'', false)'.
   client close ].

Now we can query the contents of the simple table we just created.

(P3Client new url: 'psql://sven@localhost') in: [ :client |
   [ client query: 'SELECT * FROM table1' ] ensure: [ client close ] ].

The result is an instance of P3Result

   a P3Result('SELECT 2' 2 records 3 columns)

P3Result contains 3 elements, results, descriptions & data:

  • Results is a string (collection of strings for multiple embedded queries) indicating successful execution.
  • Descriptions is a collection of row field description objects.
  • Data is a collection of rows with fully converted field values as objects.

The data itself is an array with 2 sub arrays, one for each record.

#( #(1 'foo' true) #(2 'bar' false) )

Finally we can clean up.

(P3Client new url: 'psql://sven@localhost') in: [ :client |
   [ client execute: 'DROP TABLE table1' ] ensure: [ client close ] ].

References

Using Prepared and Formatted Statements

Although you are free to create your SQL statements in any way you see fit, feeding them to #execute: and #query:, inserting arguments in SQL statements can be hard (because you have to know the correct syntax), error-prone (because you might violate syntax rules) and dangerous (due to SQL injection attacks).

P3 can help here with two mechanisms: prepared and formatted statements. They are mostly polymorphic and use the same template notation. They allow you to create a statement once, specifying placeholders with $n, and execute it once or multiple times with concrete arguments, with the necessary conversions happening automatically.

The difference between the two is that formatted statements are implemented using simple textual substitution on the client side, while prepared statements are evaluated on the server side with full syntax checking, and are executed with more type checks. Prepared statements are more efficient since the server can do part of its optimization in the prepare phase, saving time on each execution.

Here is a transcript of how to use them. First we set up a client and create a test table.

client := P3Client new url: 'psql://sven@localhost'.

client execute: 'DROP TABLE IF EXISTS table1'.
client execute: 'CREATE TABLE table1 (id INTEGER, name TEXT, weight REAL, enabled BOOLEAN)'.

Next we insert some data and then query it using prepared statements.

statement := client prepare: 'INSERT INTO table1 (id, name, weight, enabled) VALUES ($1, $2, $3, $4)'.

statement execute: { 1. 'foo'. 75.5. true }.
statement executeBatch: { { 2. 'bar'. 80.25. true }. { 3. 'foobar'. 10.75. false } }.

statement close.

statement := client prepare: 'SELECT id, name, weight FROM table1 WHERE id = $1 AND enabled = $2'.
statement query: { 1. true }.

statement close.

Note that prepared statements are server side resources that need to be closed when no longer needed. Prepared statements exist in the scope of a single session/connection.

Next we start over and do the same insert and query using formatted statements.

client execute: 'TRUNCATE TABLE table1'.

statement := client format: 'INSERT INTO table1 (id, name, weight, enabled) VALUES ($1, $2, $3, $4)'.

statement execute: { 1. 'foo'. 75.5. true }.
statement executeBatch: { { 2. 'bar'. 80.25. true }. { 3. 'foobar'. 10.75. false } }.

statement := client format: 'SELECT id, name, weight FROM table1 WHERE id = $1 AND enabled = $2'.
statement query: { 1. true }.

And finally we clean up.

client execute: 'DROP TABLE table1'.
client close.

Supported Data Types

P3 supports most common PostgreSQL types. Here are some tables with the details. As of PostgreSQL 9.6, there are 41 general purpose data types of which 32 are currently implemented.

These are the 32 general purpose data type currently implemented, with the Pharo class they map to.

Name Alias Description Oid Class
bigint int8 signed eight-byte integer 20 Integer
bigserial serial8 autoincrementing eight-byte integer 20 Integer
bit [n] fixed-length bit string 1560 P3FixedBitString
bit varying varbit variable-length bit string 1562 P3BitString
boolean bool logical boolean (true/false) 16 Boolean
box rectangular box on a plane (upperright, lowerleft) 603 P3Box
bytea binary data (byte array) 17 ByteArray
character [n] char fixed-length character string 1042 String
character varying varchar variable-length character string 1043 String
circle circle on a plane (center, radius) 718 P3Circle
date calendar date (year,month,day) 1082 Date
double precision float8 double precision floating point number (8 bytes) 701 Float
integer int, int4 signed four-byte integer 23 Integer
interval time span 114 P3Interval
json textual JSON data 114 NeoJSONObject
jsonb binary JSON data, decomposed 3802 NeoJSONObject
line infinite line on a plane (ax+by+c=0) 628 P3Line
lseg line segment on a plane (start,stop) 601 P3LineSegment
numeric decimal exact number of selectable precision 1700 ScaledDecimal
path geometric path on a plane (points) 602 P3Path
point geometric point on a plane (x, y) 600 P3Point
polygon closed geometric path on a plane (points) 604 P3Polygon
real float4 single-precision floating point number (4-bytes) 700 Float
smallint int2 signed two-byte integer 21 Integer
smallserial serial2 autoincrementing two-byte integer 21 Integer
serial serial4 autoincrementing four-byte integer 23 Integer
text variable-length character string 25 String
time [ without time zone ] time of day (no time zone) 1083 Time
time with time zone timetz time of day including time zone 1266 Time
timestamp [ without time zone ] date and time (no time zone) 1114 DateAndTime
timestamp with time zone timestamptz date and time includig time zone 1184 DateAndTime
uuid universal unique identifier 2950 UUID

Here are the 9 general purpose data types that are not yet implemented.

Name Description Oid
cidr IPv4 or IPv6 network address 650
inet IPv4 or IPv6 host address 869
macaddr MAC (Media Access Control) address 829
money currency amount 790
pg_lsn PostgreSQL Log Sequence Number 3220
tsquery text search query 3615
tsvector text search document 3614
txid_snapshot user-level transaction ID snapshot 2970
xml XML data 142

Additionally, the following 10 common types are also implemented, with the Pharo class they map to.

Name Description Oid Class
oid object identifier 26 Integer
name name 19 String
bpchar text 1042 String
void void 2278 UndefinedObject
_bool boolean array 1000 Array<Boolean>
_int4 integer array 1007 Array<Integer>
_oid oid array 1028 Array<Integer>
_text string array 1009 Array<String>
_varchar string array 1015 Array<String>
_float8 float array 1022 Array<Float>

P3 also supports enums. Each enum definition creates a new type. You can send #loadEnums to P3Client to create mappings for all visible enums.

When you do a query that results in data of an unknown type you will get an error, P3 cannot convert typeOid XXX, where XXX is the oid in the pg_type table.

Connection and Authentication

P3 connects over the network (TCP) to PostgreSQL and supports plain (#connect) and TLS/SSL encrypted (#connectSSL) connections.

It is out of the scope of this README to explain how to install and configure an advanced database like PostgreSQL. There is extensive high quality documentation available covering all aspect of PostgreSQL, see https://postgresql.org

Out of the box, most PostgreSQL installations do not allow for network connections from other machines, only for local connections. Check the listen_addresses directive in postgresql.conf

As for authentication, CleartextPassword, MD5Password and SCRAM-SHA-256 are supported. This means that SCMCredential, GSS, SSPI are currently not (yet) supported. An error will be signalled when the server requests an unsupported authentication.

You have to create database users, called roles and give them a password. In SQL you can do this with CREATE|ALTER ROLE user1 LOGIN PASSWORD 'secret'

Next you have to tell PostgreSQL how network users should authenticate themselves. This is done by editing pg_hba.conf choosing specific methods, trust (no password, no authentication), password, md5 and scram-sha-256 work with P3.

Note that for SCRAM-SHA-256 to work, you need to change the password_encryption directive in postgresql.conf to scam-sha-256, restart and reenter all user passwords.

Glorp

Included is P3DatabaseDriver, an interface between Glorp, an advanced object-relational mapper, and P3Client.

To install this driver (after loading Glorp itself), do

PharoDatabaseAccessor DefaultDriver: P3DatabaseDriver.

Configure your session using a Glorp Login object

Login new
   database: PostgreSQLPlatform new;
   username: 'username';
   password: 'password';
   connectString: 'host:5432_databasename';
   encodingStrategy: #utf8;
   yourself.

Code loading

The default group loads P3Client and its basic dependencies NeoJSON and ZTimestamp

Metacello new
   baseline: 'P3';
   repository: 'github://svenvc/P3';
   load.

The glorp group loads P3DatabaseDriver and the whole of Glorp (warning: large download)

Metacello new
   baseline: 'P3';
   repository: 'github://svenvc/P3';
   load: 'glorp'.

Unit tests

P3ClientTest holds unit tests for the P3 PSQL client.

Configure it by setting its class side's connection URL.

P3ClientTest url: 'psql://sven:secret@localhost:5432/database'.

The minimal being the following:

P3ClientTest url: 'psql://sven@localhost'.

Logging

P3 uses object logging, an advanced form of code instrumentation. This means that during execution instances of subclasses of P3LogEvent are created (some including timing information) and sent to an Announcer (accessible via P3LogEvent announcer). Interested parties can subscribe to these log events and use the information contained in them to learn about P3 code execution.

The standard print method of a P3LogEvent can be used to generate textual output. The following expression enables logging to the Transcript.

P3LogEvent logToTranscript.

Executing the four expressions of the Basic Usage section yields the following output.

2020-09-21 16:27:57 001 [P3] 63731 #Connect sven@localhost:5432 Trust
2020-09-21 16:27:57 002 [P3] 63731 #Query SELECT 565 AS N
2020-09-21 16:27:57 003 [P3] 63731 #Result SELECT 1, 1 record, 1 colum, 4 ms
2020-09-21 16:27:57 004 [P3] 63731 #Close

2020-09-21 16:28:07 005 [P3] 63733 #Connect sven@localhost:5432 Trust
2020-09-21 16:28:07 006 [P3] 63733 #Query DROP TABLE IF EXISTS table1
2020-09-21 16:28:07 007 [P3] 63733 #Error P3Notification PostgreSQL table "table1" does not exist, skipping
2020-09-21 16:28:07 008 [P3] 63733 #Result DROP TABLE, 6 ms
2020-09-21 16:28:07 009 [P3] 63733 #Query CREATE TABLE table1 (id INTEGER, name TEXT, enabled BOOLEAN)
2020-09-21 16:28:07 010 [P3] 63733 #Result CREATE TABLE, 50 ms
2020-09-21 16:28:07 011 [P3] 63733 #Query INSERT INTO table1 (id, name, enabled) VALUES (1, 'foo', true)
2020-09-21 16:28:07 012 [P3] 63733 #Result INSERT 0 1, 4 ms
2020-09-21 16:28:07 013 [P3] 63733 #Query INSERT INTO table1 (id, name, enabled) VALUES (2, 'bar', false)
2020-09-21 16:28:07 014 [P3] 63733 #Result INSERT 0 1, 0 ms
2020-09-21 16:28:07 015 [P3] 63733 #Close

2020-09-21 16:28:20 016 [P3] 63737 #Connect sven@localhost:5432 Trust
2020-09-21 16:28:20 017 [P3] 63737 #Query SELECT * FROM table1
2020-09-21 16:28:20 018 [P3] 63737 #Result SELECT 2, 2 records, 3 columns, 2 ms
2020-09-21 16:28:20 019 [P3] 63737 #Close

2020-09-21 16:39:52 020 [P3] 63801 #Connect sven@localhost:5432 Trust
2020-09-21 16:39:52 021 [P3] 63801 #Query DROP TABLE table1
2020-09-21 16:39:52 022 [P3] 63801 #Result DROP TABLE, 13 ms
2020-09-21 16:39:52 023 [P3] 63801 #Close

Remember that the information inside the log events can be used to build other applications.

Development, Goals, Contributing

The main goal of P3 is to be a modern, lean and mean PostgreSQL client for Pharo. Right now, P3 is functional and usable.

The quality of open source software is determined by it being alive, supported and maintained.

The first way to help is to simply use P3 in your projects and tells us about your successes and the issues that you encounter. You can ask questions on the Pharo mailing lists.

Development happens on GitHub, where you can create issues.

Contributions should be done with pull requests solving specific issues.

p3's People

Contributors

chisandrei avatar deem0n avatar draagrenkirneh avatar gcotelli avatar henriknergaard-ins avatar jurajkubelka avatar jvdsandt avatar lopezca avatar mahugnon avatar pavel-krivanek avatar svenvc avatar vince-refiti avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

p3's Issues

Prepared statements error when query is long and differs slightly at the end

Hello, psql prepared statements are identified by "name" (https://www.postgresql.org/docs/current/view-pg-prepared-statements.html), but in P3 client, the whole query is used as name - but, psql prepared statement name is limited :(

Imagine this two queries:
select id, name, enabled from table1 where name = ''123'' and id = 1
This query is prepared - OK.
Second query:
select id, name, enabled from table1 where name = ''123'' and id = 2
Bang! PostgreSQL error message: P3Error PostgreSQL prepared statement "select id, name, enabled from table1 where name = '123' and id = 2" already exists

How to solve this? Add second peramater to the P3Client prepare: or maybe better, use some sort of hashes as prepared statement names internally?

Additional types

Using glorp, I am trying to specify platform serial or platform bigint and realising that neither exists. The serial type in my experience seems pretty standard for primary key use. Would it be hard to add support for these? I understand that array support may be a bit more involved but is also rather more rare in use than serial types.

Bug with Pharo 7

I just tested P3 with Pharo 7 and I got the following error message : Instance of ZnCharacterReadStream did not understand #binary.

The error is fired in ZTimezone >> fromFile: file

README.md typo

In the Unit tests section, you have the class name as P3ClientTests, whereas it should be P3ClientTest

Reset in P3Client>>connect drop password

Just a few minutes in to trying P3. After doing...
Metacello new
baseline: 'P3';
repository: 'github://svenvc/P3';
load.

Doing...
(P3Client new url: 'psql://postgres:xxxxx@localhost:5432/nyc_data') in: [ :client |
[ client isWorking ] ensure: [ client close ] ].
produces... "P3Error PostgreSQL Password required"

Tracing through, in P3CLient>>connect I find...
self ==> a P3Client(psql://postgres:xxxxxx@localhost:5432/nyc_data)
before sending #reset, and after...
self ==> a P3Client(psql://localhost:5432/)

Now it works if I comment out "self reset".

Can't get P3 working after migration from Pharo 8 to 9

Hi,
I had to migrate from Pharo 8 to 9 and re-installed P3 with the following scripts (the same i had used to install P3 in Pharo 8)

Metacello new
baseline: 'P3';
repository: 'github://svenvc/P3';
load.

and

Metacello new
baseline: 'P3';
repository: 'github://svenvc/P3';
load: 'glorp'.

I also re-installed DBXTalk/Glorp/main
and "do-it" : PharoDatabaseAccessor DefaultDriver: P3DatabaseDriver.

Before that i installed Spec2.

Then i tried to run this script
(P3Client new url: 'psql://Richard:lulosa.2MA@localhost:5432/Glazux') in: [ :client |
[ client query: 'SELECT * FROM ingredients' ] ensure: [ client close ] ].

but got this error:
P3-Pbm

I probably forgot to do something but i don't remember what. Do i need to install a MetaRepoForPharo? But which version?
Thanks for help
Richard
(btw: the same script runs ok in Pharo 8)

custom P3 error classes

The error handling in P3 uses general error: calls on many places when something fails. It should signal custom errors with own classes so it can be more easily tested, converted and translated.

Use of pgcrypto, can't call crypt?

I'm attempting to use pgcrypto, specifically this: select crypt('password', gen_salt('bf')) and I'm unable to make it work. I've tried with prepared statements and formatted statements alike trying to replace 'bf' with a parameter $2 and trying to just leave it inline with the single quotes escaped like so: gen_salt(''bf''). I thought this would be straightforward but I'm a bit stumped.

Can't handle array type

Trying to select a _varchar (array) column I get this Error (cannot convert typeOid 1015):

P3Converter(Object)>>error:
[ self
	error: 'P3 cannot convert typeOid ' , description typeOid asString ] in P3Converter>>convert:length:description:
IdentityDictionary(Dictionary)>>at:ifAbsent:
P3Converter>>convert:length:description:
P3RowFieldDescription>>convert:length:using:
P3Client>>processDataRowUsing:
[ :out | 
[ self readMessage tag = $C ]
	whileFalse: [ self assert: message tag = $D.
		out nextPut: (self processDataRowUsing: result descriptions) ] ] in P3Client>>runQueryResult
Array class(SequenceableCollection class)>>new:streamContents:
Array class(SequenceableCollection class)>>streamContents:
P3Client>>runQueryResult
P3Client>>query:
[ client
	query: 'select id,locale,created_at,confidential,rights from users limit 3' ] in [ :client | 
[ client
	query: 'select id,locale,created_at,confidential,rights from users limit 3' ]
	ensure: [ client close ] ] in UndefinedObject>>DoIt
BlockClosure>>ensure:
[ :client | 
[ client
	query: 'select id,locale,created_at,confidential,rights from users limit 3' ]
	ensure: [ client close ] ] in UndefinedObject>>DoIt
P3Client(Object)>>in:
UndefinedObject>>DoIt
OpalCompiler>>evaluate
RubSmalltalkEditor>>evaluate:andDo:
RubSmalltalkEditor>>highlightEvaluateAndDo:
[ textMorph textArea editor highlightEvaluateAndDo: ann action.
textMorph shoutStyler style: textMorph text ] in [ textMorph textArea
	handleEdit: [ textMorph textArea editor highlightEvaluateAndDo: ann action.
		textMorph shoutStyler style: textMorph text ] ] in GLMMorphicPharoScriptRenderer(GLMMorphicPharoCodeRenderer)>>actOnHighlightAndEvaluate:
RubEditingArea(RubAbstractTextArea)>>handleEdit:
[ textMorph textArea
	handleEdit: [ textMorph textArea editor highlightEvaluateAndDo: ann action.
		textMorph shoutStyler style: textMorph text ] ] in GLMMorphicPharoScriptRenderer(GLMMorphicPharoCodeRenderer)>>actOnHighlightAndEvaluate:
WorldState>>runStepMethodsIn:
WorldMorph>>runStepMethods
WorldState>>doOneCycleNowFor:
WorldState>>doOneCycleFor:
WorldMorph>>doOneCycle
WorldMorph class>>doOneCycle
[ [ WorldMorph doOneCycle.
Processor yield.
false ] whileFalse: [  ] ] in MorphicUIManager>>spawnNewProcess
[ self value.
Processor terminateActive ] in BlockClosure>>newProcess

connect to PGSQL 10.6. but error with Timezone class

server : postgresql 10.6 with gentoo linux
pharo : pharo 7.0

test code :

(P3Client new url: 'psql://frontend:[email protected]:5432/pegarelease') in: [ :client |
   [ client query: 'SELECT * FROM system_countries' ] ensure: [ client close ] ].

but encounterd this error...

readTimezoneWithId : [ ^ self error: 'Unknown Timezone ID: ' , id asString ]

id Value is "ROK". how to solve this?

Using with glorp

Pharo 7 latest 32 bit, Ubuntu 16.04LTS, local postgres install

Following the glorp booklet, and adapting it to P3.

Metacello new
baseline: 'P3';
repository: 'github://svenvc/P3';
load: 'glorp'.

PharoDatabaseAccessor DefaultDriver: P3DatabaseDriver.

(section 4.5)
login := Login new
database: PostgreSQLPlatform new;
username: 'un';
password: 'pw';
connectString: 'localhost:5432_stephan';
encodingStrategy: #utf8;
yourself.
session := GlorpBookDescriptorSystem sessionForLogin: login.
session login

gives a
Instance of P3Client did not understand #queryEncoding

Problems with setting the parameters of P3PreparedStatement

Currently the P3PreparedStatement >> #bindString: method just sends asString to each parameter. This works for some objects like Strings, Integers and Booleans, but not for ByteArrays, ScaledDecimals and probably some others.

I think we need a more intelligent conversion strategy. I see two options:

  1. Dispatch to a convertParameter method in P3Converter to make to conversion dependent on the parameter type.
  2. Make use of an extension method to do the conversion with a default implementation in Object. The extension method can be something like:
writeAsP3TextParameterOn: aStream type: aParameterDescription 
	"default implementation"
	self printOn: aStream

And for ByteArray:

writeAsP3TextParameterOn: aStream type: aParameterDescription 
	aStream nextPutAll: '\x'; nextPutAll: self hex

I think the seconds option is the most flexible but this option does require adding some class extensions.

Question about ip addr support

Hi, I can not find standard class in Pharo for IPv4 and IPv6 addresses, so probably we can just use text representation for PostgreSQL type inet and friends ?

If that is accepted, I could make a PR.

Tag reading when the database does not exist

When the database you try to connect does not exist, the method
P3MessageBuffer>>#readFrom: (called from P3Client>>#readMessageOnError:)
fails because the readStream next returns nil and the internal error handling does not expect such case.

Custom column type

I have the following custom column type: CREATE TYPE public.money_with_currency AS (currency_code char(3), amount numeric(20,8))

When running: select * from table with that column, I'm getting: P3Error Cannot convert type with OID 23409

Do you have any suggestions on how I could pass that type to P3?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.